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..7bda179a11131 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -408,6 +408,7 @@ protected function saveAndReplaceAdvancedPrices() } elseif (\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); @@ -562,11 +563,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 +595,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/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..505e178f49f43 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.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="AdminAdvancedReportingButtonTest"> + <annotations> + <stories value="AdvancedReporting"/> + <title value="AdvancedReportingButtonTest"/> + <description value="Test log in to AdvancedReporting and tests AdvancedReportingButtonTest"/> + <testCaseId value="MC-14800"/> + <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/AsynchronousOperations/Model/MassConsumer.php b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php index 28bc8141a8e99..86e691daa4213 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php @@ -9,6 +9,7 @@ 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; @@ -58,6 +59,11 @@ class MassConsumer implements ConsumerInterface */ private $operationProcessor; + /** + * @var Registry + */ + private $registry; + /** * Initialize dependencies. * @@ -67,6 +73,7 @@ class MassConsumer implements ConsumerInterface * @param ConsumerConfigurationInterface $configuration * @param OperationProcessorFactory $operationProcessorFactory * @param LoggerInterface $logger + * @param Registry $registry */ public function __construct( CallbackInvoker $invoker, @@ -74,7 +81,8 @@ public function __construct( 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/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..5bc9335d24439 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 { @@ -651,7 +654,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..357385e5c8c79 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Request.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Request.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\Directpost; @@ -10,6 +11,7 @@ /** * 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 { @@ -20,6 +22,7 @@ class Request extends AuthorizenetRequest /** * Return merchant transaction key. + * * Needed to generate sign. * * @return string @@ -31,6 +34,7 @@ protected function _getTransactionKey() /** * Set merchant transaction key. + * * Needed to generate sign. * * @param string $transKey @@ -162,6 +166,7 @@ public function setDataFromOrder( /** * Set sign hash into the request object. + * * All needed fields should be placed in the object fist. * * @return $this 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..1c713a159c3ad 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 { 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/etc/adminhtml/system.xml b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml index 1319fa102d0d8..28bf6945c8b81 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> diff --git a/app/code/Magento/Authorizenet/etc/config.xml b/app/code/Magento/Authorizenet/etc/config.xml index a960e96a288ea..02dca74023e22 100644 --- a/app/code/Magento/Authorizenet/etc/config.xml +++ b/app/code/Magento/Authorizenet/etc/config.xml @@ -19,7 +19,7 @@ <order_status>processing</order_status> <payment_action>authorize</payment_action> <test>1</test> - <title>Credit Card Direct Post (Authorize.net)</title> + <title>Credit Card Direct Post (Authorize.Net)</title> <trans_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> <trans_md5 backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> <allowspecific>0</allowspecific> 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"> <groups> <group id="authorizenet"> - <label>Authorize.net</label> + <label>Authorize.Net</label> </group> </groups> </payment> 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Block; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Backend\Model\Session\Quote; +use Magento\Framework\View\Element\Template\Context; +use Magento\Payment\Block\Form\Cc; +use Magento\Payment\Model\Config as PaymentConfig; + +/** + * Block for representing the payment form + * + * @api + */ +class Form extends Cc +{ + /** + * @var Config + */ + private $config; + + /** + * @var Quote + */ + private $sessionQuote; + + /** + * @param Context $context + * @param PaymentConfig $paymentConfig + * @param Config $config + * @param Quote $sessionQuote + * @param array $data + */ + public function __construct( + Context $context, + PaymentConfig $paymentConfig, + Config $config, + Quote $sessionQuote, + array $data = [] + ) { + parent::__construct($context, $paymentConfig, $data); + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Block; + +use Magento\Framework\Phrase; +use Magento\Payment\Block\ConfigurableInfo; + +/** + * Translates the labels for the info block + * + * @api + */ +class Info extends ConfigurableInfo +{ + /** + * Returns label + * + * @param string $field + * @return Phrase + */ + protected function getLabel($field): Phrase + { + return __($field); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Block/Payment.php b/app/code/Magento/AuthorizenetAcceptjs/Block/Payment.php new file mode 100644 index 0000000000000..059bba0c805e8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Block/Payment.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\Block; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\View\Element\Template; +use Magento\Checkout\Model\ConfigProviderInterface; +use Magento\Framework\View\Element\Template\Context; + +/** + * Represents the payment block for the admin checkout form + * + * @api + */ +class Payment extends Template +{ + /** + * @var ConfigProviderInterface + */ + private $config; + + /** + * @var Json + */ + private $json; + + /** + * @param Context $context + * @param ConfigProviderInterface $config + * @param Json $json + * @param array $data + */ + public function __construct( + Context $context, + ConfigProviderInterface $config, + Json $json, + array $data = [] + ) { + parent::__construct($context, $data); + $this->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 @@ +<?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\SubjectReader; +use Magento\Payment\Gateway\Command\CommandException; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\CommandInterface; + +/** + * Chooses the best method of accepting the payment based on the status of the transaction + */ +class AcceptPaymentStrategyCommand implements CommandInterface +{ + private const ACCEPT_FDS = 'accept_fds'; + private const NEEDS_APPROVAL_STATUSES = [ + 'FDSPendingReview', + 'FDSAuthorizedPendingReview' + ]; + + /** + * @var CommandPoolInterface + */ + private $commandPool; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param CommandPoolInterface $commandPool + * @param SubjectReader $subjectReader + */ + public function __construct( + CommandPoolInterface $commandPool, + SubjectReader $subjectReader + ) { + $this->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 @@ +<?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\SubjectReader; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\Helper\ContextHelper; +use Magento\Sales\Api\Data\OrderPaymentInterface; +use Magento\Sales\Api\Data\TransactionInterface; +use Magento\Sales\Api\TransactionRepositoryInterface; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; + +/** + * Chooses the best method of capture based on the context of the payment + */ +class CaptureStrategyCommand implements CommandInterface +{ + private const SALE = 'sale'; + private const CAPTURE = 'settle'; + + /** + * @var CommandPoolInterface + */ + private $commandPool; + + /** + * @var TransactionRepositoryInterface + */ + private $transactionRepository; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param CommandPoolInterface $commandPool + * @param TransactionRepositoryInterface $repository + * @param FilterBuilder $filterBuilder + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param SubjectReader $subjectReader + */ + public function __construct( + CommandPoolInterface $commandPool, + TransactionRepositoryInterface $repository, + FilterBuilder $filterBuilder, + SearchCriteriaBuilder $searchCriteriaBuilder, + SubjectReader $subjectReader + ) { + $this->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 @@ +<?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\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Command\CommandPool; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\Response\HandlerInterface; + +/** + * Syncs the transaction status with authorize.net + */ +class FetchTransactionInfoCommand implements CommandInterface +{ + /** + * @var CommandPool + */ + private $commandPool; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @var Config + */ + private $config; + + /** + * @var HandlerInterface|null + */ + private $handler; + + /** + * @param CommandPoolInterface $commandPool + * @param SubjectReader $subjectReader + * @param Config $config + * @param HandlerInterface|null $handler + */ + public function __construct( + CommandPoolInterface $commandPool, + SubjectReader $subjectReader, + Config $config, + HandlerInterface $handler = null + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Exception; +use Magento\Payment\Gateway\Command\CommandException; +use Magento\Payment\Gateway\Command\ResultInterface; +use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\Http\ClientInterface; +use Magento\Payment\Gateway\Http\TransferFactoryInterface; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Payment\Gateway\Validator\ValidatorInterface; +use Psr\Log\LoggerInterface; +use Magento\Payment\Gateway\Command\Result\ArrayResult; + +/** + * Makes a request to the gateway and returns results + */ +class GatewayQueryCommand implements CommandInterface +{ + /** + * @var BuilderInterface + */ + private $requestBuilder; + + /** + * @var TransferFactoryInterface + */ + private $transferFactory; + + /** + * @var ClientInterface + */ + private $client; + + /** + * @var ValidatorInterface + */ + private $validator; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param BuilderInterface $requestBuilder + * @param TransferFactoryInterface $transferFactory + * @param ClientInterface $client + * @param LoggerInterface $logger + * @param ValidatorInterface $validator + */ + public function __construct( + BuilderInterface $requestBuilder, + TransferFactoryInterface $transferFactory, + ClientInterface $client, + LoggerInterface $logger, + ValidatorInterface $validator + ) { + $this->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 @@ +<?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\SubjectReader; +use Magento\Payment\Gateway\Command\CommandException; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\CommandInterface; + +/** + * Chooses the best method of returning the payment based on the status of the transaction + */ +class RefundTransactionStrategyCommand implements CommandInterface +{ + private const REFUND = 'refund_settled'; + private const VOID = 'void'; + + /** + * @var CommandPoolInterface + */ + private $commandPool; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param CommandPoolInterface $commandPool + * @param SubjectReader $subjectReader + */ + public function __construct( + CommandPoolInterface $commandPool, + SubjectReader $subjectReader + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway; + +use Magento\AuthorizenetAcceptjs\Model\Adminhtml\Source\Environment; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Houses configuration for this gateway + */ +class Config extends \Magento\Payment\Gateway\Config\Config +{ + const METHOD = 'authorizenet_acceptjs'; + private const KEY_LOGIN_ID = 'login'; + private const KEY_TRANSACTION_KEY = 'trans_key'; + private const KEY_ENVIRONMENT = 'environment'; + private const KEY_LEGACY_TRANSACTION_HASH = 'trans_md5'; + private const KEY_SIGNATURE_KEY = 'trans_signature_key'; + private const KEY_PAYMENT_ACTION = 'payment_action'; + private const KEY_SHOULD_EMAIL_CUSTOMER = 'email_customer'; + private const KEY_ADDITIONAL_INFO_KEYS = 'paymentInfoKeys'; + private const KEY_CLIENT_KEY = 'public_client_key'; + private const KEY_CVV_ENABLED = 'cvv_enabled'; + private const KEY_TRANSACTION_SYNC_KEYS = 'transactionSyncKeys'; + private const ENDPOINT_URL_SANDBOX = 'https://apitest.authorize.net/xml/v1/request.api'; + private const ENDPOINT_URL_PRODUCTION = 'https://api.authorize.net/xml/v1/request.api'; + private const SOLUTION_ID_SANDBOX = 'AAA102993'; + private const SOLUTION_ID_PRODUCTION = 'AAA175350'; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param null|string $methodCode + * @param string $pathPattern + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + $methodCode = null, + $pathPattern = self::DEFAULT_PATH_PATTERN + ) { + parent::__construct($scopeConfig, $methodCode, $pathPattern); + } + + /** + * Gets the login id + * + * @param int|null $storeId + * @return string + */ + public function getLoginId($storeId = null): ?string + { + return $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Http; + +use InvalidArgumentException; +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Payment\Gateway\Http\ClientException; +use Magento\Payment\Gateway\Http\ClientInterface; +use Magento\Payment\Gateway\Http\TransferInterface; +use Magento\Payment\Model\Method\Logger as PaymentLogger; +use Psr\Log\LoggerInterface; + +/** + * A client that can communicate with the Authorize.net API + */ +class Client implements ClientInterface +{ + /** + * @var PaymentLogger + */ + private $paymentLogger; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var ZendClientFactory + */ + private $httpClientFactory; + + /** + * @var Config + */ + private $config; + + /** + * @var Json + */ + private $json; + + /** + * @param PaymentLogger $paymentLogger + * @param LoggerInterface $logger + * @param ZendClientFactory $httpClientFactory + * @param Config $config + * @param Json $json + */ + public function __construct( + PaymentLogger $paymentLogger, + LoggerInterface $logger, + ZendClientFactory $httpClientFactory, + Config $config, + Json $json + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Http\Payload\Filter; + +use Magento\AuthorizenetAcceptjs\Gateway\Http\Payload\FilterInterface; + +/** + * Removes a set of fields from the payload + */ +class RemoveFieldsFilter implements FilterInterface +{ + /** + * @var array + */ + private $fields; + + /** + * @param array $fields + */ + public function __construct(array $fields) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Http\Payload; + +/** + * Describes a filter for filtering content after all the builders have finished + */ +interface FilterInterface +{ + /** + * Filters some data before use + * + * @param array $data + * @return array + */ + public function filter(array $data): array; +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/TransferFactory.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/TransferFactory.php new file mode 100644 index 0000000000000..a4cdeba77492c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/TransferFactory.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Http; + +use Magento\Payment\Gateway\Http\TransferBuilder; +use Magento\Payment\Gateway\Http\TransferFactoryInterface; +use Magento\Payment\Gateway\Http\TransferInterface; +use Magento\AuthorizenetAcceptjs\Gateway\Http\Payload\FilterInterface; + +/** + * Can create a transfer object + */ +class TransferFactory implements TransferFactoryInterface +{ + /** + * @var TransferBuilder + */ + private $transferBuilder; + + /** + * @var array + */ + private $payloadFilters; + + /** + * @param TransferBuilder $transferBuilder + * @param FilterInterface[] $payloadFilters + */ + public function __construct( + TransferBuilder $transferBuilder, + array $payloadFilters = [] + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the meta transaction information to the request + */ +class AcceptFdsDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; + +/** + * Adds the basic payment information to the request + */ +class AddressDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Payment\Helper\Formatter; + +/** + * Adds the amount of the transaction to the Request + */ +class AmountDataBuilder implements BuilderInterface +{ + use Formatter; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; + +/** + * Adds the stored credentials to the request + */ +class AuthenticationDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @var Config + */ + private $config; + + /** + * @param SubjectReader $subjectReader + * @param Config $config + */ + public function __construct( + SubjectReader $subjectReader, + Config $config + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the meta transaction information to the request + */ +class AuthorizeDataBuilder implements BuilderInterface +{ + private const REQUEST_AUTH_ONLY = 'authOnlyTransaction'; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @var PassthroughDataObject + */ + private $passthroughData; + + /** + * @param SubjectReader $subjectReader + * @param PassthroughDataObject $passthroughData + */ + public function __construct( + SubjectReader $subjectReader, + PassthroughDataObject $passthroughData + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the meta transaction information to the request + */ +class CaptureDataBuilder implements BuilderInterface +{ + private const REQUEST_TYPE_PRIOR_AUTH_CAPTURE = 'priorAuthCaptureTransaction'; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @var PassthroughDataObject + */ + private $passthroughData; + + /** + * @param SubjectReader $subjectReader + * @param PassthroughDataObject $passthroughData + */ + public function __construct( + SubjectReader $subjectReader, + PassthroughDataObject $passthroughData + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; + +/** + * Adds the custom settings to the request + */ +class CustomSettingsBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @var Config + */ + private $config; + + /** + * @param SubjectReader $subjectReader + * @param Config $config + */ + public function __construct(SubjectReader $subjectReader, Config $config) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; + +/** + * Adds the basic payment information to the request + */ +class CustomerDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; + +/** + * Adds the basic payment information to the request + */ +class OrderDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use Magento\Payment\Gateway\Request\BuilderInterface; + +/** + * Adds data to the request that can be used in the response + */ +class PassthroughDataBuilder implements BuilderInterface +{ + /** + * @var PassthroughDataObject + */ + private $passthroughData; + + /** + * @param PassthroughDataObject $passthroughData + */ + public function __construct(PassthroughDataObject $passthroughData) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the basic payment information to the request + */ +class PaymentDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the basic payment information to the request + */ +class PoDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the basic refund information to the request + */ +class RefundPaymentDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the reference transaction to the request + */ +class RefundReferenceTransactionDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\Payment\Gateway\Request\BuilderInterface; + +/** + * Adds the meta transaction information to the request + */ +class RefundTransactionTypeDataBuilder implements BuilderInterface +{ + private const REQUEST_TYPE_REFUND = 'refundTransaction'; + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + return [ + 'transactionRequest' => [ + '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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\Payment\Gateway\Request\BuilderInterface; + +/** + * Adds the type of the request to the build subject + */ +class RequestTypeBuilder implements BuilderInterface +{ + /** + * @var string + */ + private $type; + + /** + * @param string $type + */ + public function __construct(string $type) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the meta transaction information to the request + */ +class SaleDataBuilder implements BuilderInterface +{ + private const REQUEST_AUTH_AND_CAPTURE = 'authCaptureTransaction'; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @var PassthroughDataObject + */ + private $passthroughData; + + /** + * @param SubjectReader $subjectReader + * @param PassthroughDataObject $passthroughData + */ + public function __construct( + SubjectReader $subjectReader, + PassthroughDataObject $passthroughData + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the shipping information to the request + */ +class ShippingDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; + +/** + * Adds the appropriate solution ID to the request + */ +class SolutionDataBuilder implements BuilderInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + * @param Config $config + */ + public function __construct(SubjectReader $subjectReader, Config $config) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; + +/** + * This builder is used for correct store resolving and used only to retrieve correct store ID. + */ +class StoreConfigBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the reference transaction to the request + */ +class TransactionDetailsDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the meta transaction information to the request + */ +class VoidDataBuilder implements BuilderInterface +{ + private const REQUEST_TYPE_VOID = 'voidTransaction'; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct( + SubjectReader $subjectReader + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Processes payment information from a void transaction response + */ +class CloseParentTransactionHandler implements HandlerInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Processes payment information from a void transaction response + */ +class CloseTransactionHandler implements HandlerInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Processes payment information from a response + */ +class PaymentResponseHandler implements HandlerInterface +{ + private const RESPONSE_CODE_HELD = 4; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Processes payment information from a void transaction response + */ +class PaymentReviewStatusHandler implements HandlerInterface +{ + private const REVIEW_PENDING_STATUSES = [ + 'FDSPendingReview', + 'FDSAuthorizedPendingReview' + ]; + private const REVIEW_DECLINED_STATUSES = [ + 'void', + 'declined' + ]; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionResponseValidator; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Adds the details to the transaction that should show when the transaction is viewed in the admin + */ +class TransactionDetailsResponseHandler implements HandlerInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @var Config + */ + private $config; + + /** + * @param SubjectReader $subjectReader + * @param Config $config + */ + public function __construct(SubjectReader $subjectReader, Config $config) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Processes transaction id for the payment + */ +class TransactionIdHandler implements HandlerInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; + +/** + * Processes payment information from a void transaction response + */ +class VoidResponseHandler implements HandlerInterface +{ + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway; + +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Gateway\Helper; + +/** + * Helper for extracting information from the payment data structure + */ +class SubjectReader +{ + /** + * Reads payment from subject + * + * @param array $subject + * @return PaymentDataObjectInterface + */ + public function readPayment(array $subject): PaymentDataObjectInterface + { + return Helper\SubjectReader::readPayment($subject); + } + + /** + * Reads store's ID, otherwise returns null. + * + * @param array $subject + * @return int|null + */ + public function readStoreId(array $subject): ?int + { + $storeId = $subject['store_id'] ?? null; + + if (empty($storeId)) { + try { + $storeId = (int)$this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Validator; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Validator\AbstractValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; + +/** + * Validates that the request was successful + */ +class GeneralResponseValidator extends AbstractValidator +{ + /** + * The result code that authorize.net returns for a successful Api call + */ + private const RESULT_CODE_SUCCESS = 'Ok'; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @var ResultInterfaceFactory + */ + private $resultFactory; + + /** + * @param ResultInterfaceFactory $resultFactory + * @param SubjectReader $subjectReader + */ + public function __construct( + ResultInterfaceFactory $resultFactory, + SubjectReader $subjectReader + ) { + parent::__construct($resultFactory); + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Validator; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Framework\Encryption\Helper\Security; +use Magento\Payment\Gateway\Validator\AbstractValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; + +/** + * Validates the transaction hash + */ +class TransactionHashValidator extends AbstractValidator +{ + /** + * The error code for failed transaction hash verification + */ + private const ERROR_TRANSACTION_HASH = 'ETHV'; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @var Config + */ + private $config; + + /** + * @param ResultInterfaceFactory $resultFactory + * @param SubjectReader $subjectReader + * @param Config $config + */ + public function __construct( + ResultInterfaceFactory $resultFactory, + SubjectReader $subjectReader, + Config $config + ) { + parent::__construct($resultFactory); + + $this->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..93b5f2bb62a7d --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Validator; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Validator\AbstractValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; + +/** + * Validates the status of an attempted transaction + */ +class TransactionResponseValidator extends AbstractValidator +{ + private const RESPONSE_CODE_APPROVED = 1; + private const RESPONSE_CODE_HELD = 4; + private const RESPONSE_REASON_CODE_APPROVED = 1; + private const RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED = 252; + private const RESPONSE_REASON_CODE_PENDING_REVIEW = 253; + + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @param ResultInterfaceFactory $resultFactory + * @param SubjectReader $subjectReader + */ + public function __construct(ResultInterfaceFactory $resultFactory, SubjectReader $subjectReader) + { + parent::__construct($resultFactory); + $this->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 ($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['errorCode']; + } + } + + 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 <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/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 <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/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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Model\Adminhtml\Source; + +use Magento\Payment\Model\Source\Cctype as PaymentCctype; + +/** + * Authorize.net Payment CC Types Source Model + */ +class Cctype extends PaymentCctype +{ + /** + * @inheritdoc + */ + public function getAllowedTypes(): array + { + return ['VI', 'MC', 'AE', 'DI', 'JCB', 'DN']; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Environment.php b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Environment.php new file mode 100644 index 0000000000000..f2eca8e143916 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Environment.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Model\Adminhtml\Source; + +/** + * Authorize.net Environment Dropdown source + */ +class Environment implements \Magento\Framework\Data\OptionSourceInterface +{ + const ENVIRONMENT_PRODUCTION = 'production'; + const ENVIRONMENT_SANDBOX = 'sandbox'; + + /** + * Possible environment types + * + * @return array + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Model\Adminhtml\Source; + +/** + * Authorize.net Payment Action Dropdown source + */ +class PaymentAction implements \Magento\Framework\Data\OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => '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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Model; + +use Magento\Framework\DataObject; + +/** + * Contains all the accumulated data from the request builders that should be passed through to the handlers + */ +class PassthroughDataObject extends DataObject +{ + +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/Ui/ConfigProvider.php b/app/code/Magento/AuthorizenetAcceptjs/Model/Ui/ConfigProvider.php new file mode 100644 index 0000000000000..b24c101a3f792 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/Ui/ConfigProvider.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Model\Ui; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Checkout\Model\ConfigProviderInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Retrieves config needed for checkout + */ +class ConfigProvider implements ConfigProviderInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @var CartInterface + */ + private $cart; + + /** + * @param Config $config + * @param CartInterface $cart + */ + public function __construct(Config $config, CartInterface $cart) + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Observer; + +use Magento\Framework\Event\Observer; +use Magento\Payment\Observer\AbstractDataAssignObserver; +use Magento\Quote\Api\Data\PaymentInterface; + +/** + * Adds the payment info to the payment object + */ +class DataAssignObserver extends AbstractDataAssignObserver +{ + /** + * @var array + */ + private $additionalInformationList = [ + 'opaqueDataDescriptor', + 'opaqueDataValue', + 'ccLast4' + ]; + + /** + * @inheritdoc + */ + public function execute(Observer $observer) + { + $data = $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Setup\Patch\Data; + +use Magento\Framework\App\Config\ConfigResource\ConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Api\Data\WebsiteInterface; + +/** + * Copies the Authorize.net DirectPost configuration values to the new Accept.js module. + */ +class CopyCurrentConfig implements DataPatchInterface +{ + private const DIRECTPOST_PATH = 'payment/authorizenet_directpost'; + private const ACCEPTJS_PATH = 'payment/authorizenet_acceptjs'; + private const PAYMENT_PATH_FORMAT = '%s/%s'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ConfigInterface + */ + private $resourceConfig; + + /** + * @var EncryptorInterface + */ + private $encryptor; + + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var array + */ + private $configFieldsToMigrate = [ + 'cctypes', + 'debug', + 'email_customer', + 'order_status', + 'payment_action', + 'currency', + 'allow_specific', + 'specificcountry', + 'min_order_total', + 'max_order_total' + ]; + + /** + * @var array + */ + private $encryptedConfigFieldsToMigrate = [ + 'login', + 'trans_key', + 'trans_md5' + ]; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param ScopeConfigInterface $scopeConfig + * @param ConfigInterface $resourceConfig + * @param EncryptorInterface $encryptor + * @param StoreManagerInterface $storeManager + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + ScopeConfigInterface $scopeConfig, + ConfigInterface $resourceConfig, + EncryptorInterface $encryptor, + StoreManagerInterface $storeManager + ) { + $this->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 @@ +<?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="ConfigureAuthorizenetAcceptjs"> + <arguments> + <argument name="paymentAction" type="string"/> + </arguments> + <!-- Navigate to configuration --> + <waitForPageLoad stepKey="waitForStores"/> + <click stepKey="clickOnStores" selector="{{AdminMenuSection.stores}}"/> + <waitForPageLoad stepKey="waitForConfiguration"/> + <click stepKey="clickOnConfiguration" selector="{{StoresSubmenuSection.configuration}}"/> + <waitForPageLoad stepKey="waitForSales"/> + <waitForElementVisible stepKey="waitForVisibleHack" selector="{{AdminMenuSection.currencySetup}}"/> + <scrollTo stepKey="scrollToSales" selector="{{StoresConfigurationListSection.sales}}"/> + <click stepKey="clickOnSales" selector="{{StoresConfigurationListSection.sales}}" /> + <waitForPageLoad stepKey="waitForPaymentMethods"/> + <click stepKey="clickOnPaymentMethods" selector="{{StoresConfigurationListSection.salesPaymentMethods}}" /> + <waitForPageLoad stepKey="waitForOpenConfiguration"/> + <scrollTo stepKey="scrollToOpenConfig" selector="{{AuthorizenetAcceptjsConfigurationSection.openSectionToggle}}"/> + <conditionalClick stepKey="openConfiguration" selector="{{AuthorizenetAcceptjsConfigurationSection.openSectionToggle}}" dependentSelector="{{AuthorizenetAcceptjsConfigurationSection.alreadyOpenSectionToggle}}" visible="false"/> + + <!-- Fill Auth.net fields and save --> + <waitForPageLoad stepKey="waitToFillApiLogin"/> + <conditionalClick selector="{{AuthorizenetAcceptjsConfigurationSection.paymentActionCheckbox}}" stepKey="uncheckPaymentActionDefault" dependentSelector="{{AuthorizenetAcceptjsConfigurationSection.paymentActionSelectDisabled}}" visible="true"/> + <selectOption selector="{{AuthorizenetAcceptjsConfigurationSection.paymentActionSelect}}" stepKey="selectPaymentAction" userInput="{{paymentAction}}"/> + <scrollTo stepKey="scrollToApiLoginId" selector="{{AuthorizenetAcceptjsConfigurationSection.apiLoginIdField}}"/> + <fillField stepKey="fillApiLoginId" selector="{{AuthorizenetAcceptjsConfigurationSection.apiLoginIdField}}" userInput="{{_CREDS.authorizenet_acceptjs_api_login_id}}"/> + <fillField stepKey="fillTransactionKey" selector="{{AuthorizenetAcceptjsConfigurationSection.transactionKeyField}}" userInput="{{_CREDS.authorizenet_acceptjs_transaction_key}}"/> + <fillField stepKey="fillPublicClientKey" selector="{{AuthorizenetAcceptjsConfigurationSection.publicClientKeyField}}" userInput="{{_CREDS.authorizenet_acceptjs_public_client_key}}"/> + <fillField stepKey="fillSignatureKey" selector="{{AuthorizenetAcceptjsConfigurationSection.signatureKeyField}}" userInput="{{_CREDS.authorizenet_acceptjs_signature_key}}"/> + <uncheckOption stepKey="uncheckCheckbox" selector="{{AuthorizenetAcceptjsConfigurationSection.enabledDefaultCheckbox}}"/> + <selectOption stepKey="fillExpYear" selector="{{AuthorizenetAcceptjsConfigurationSection.enabledDefaultSelect}}" userInput="Yes"/> + <click stepKey="clickOnSave" selector="{{ConfigurationMainActionsSection.save}}" /> + </actionGroup> + + <actionGroup name="DisableAuthorizenetAcceptjs"> + <magentoCLI stepKey="disableAuthorizenetAcceptjs" command="config:set payment/authorizenet_acceptjs/active 0"/> + </actionGroup> +</actionGroups> 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 @@ +<?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="FillPaymentInformation"> + <click stepKey="clickOnAuthorizenetToggle" selector="{{AuthorizenetCheckoutSection.selectAuthorizenet}}"/> + <waitForPageLoad stepKey="waitForCardDataSection"/> + <fillField stepKey="fillCardNumber" selector="{{AuthorizenetCheckoutSection.cardInput}}" userInput="{{PaymentAndShippingInfo.cardNumber}}"/> + <selectOption stepKey="fillExpMonth" selector="{{AuthorizenetCheckoutSection.expMonth}}" userInput="{{PaymentAndShippingInfo.month}}"/> + <selectOption stepKey="fillExpYear" selector="{{AuthorizenetCheckoutSection.expYear}}" userInput="20{{PaymentAndShippingInfo.year}}"/> + <fillField stepKey="fillCvv" selector="{{AuthorizenetCheckoutSection.cvv}}" userInput="123"/> + <click stepKey="checkoutButton" selector="{{AuthorizenetCheckoutSection.checkoutButton}}"/> + <waitForPageLoad stepKey="waitForCheckout"/> + </actionGroup> + + <!-- Guest checkout Authorizenet fill billing address --> + <actionGroup name="GuestCheckoutAuthorizenetFillBillingAddress"> + <arguments> + <argument name="customer"/> + <argument name="customerAddress"/> + </arguments> + <fillField selector="{{GuestAuthorizenetCheckoutSection.firstName}}" userInput="{{customer.firstName}}" stepKey="fillFirstName"/> + <fillField selector="{{GuestAuthorizenetCheckoutSection.lastName}}" userInput="{{customer.lastName}}" stepKey="fillLastName"/> + <fillField selector="{{GuestAuthorizenetCheckoutSection.street}}" userInput="{{customerAddress.street[0]}}" stepKey="fillStreet"/> + <fillField selector="{{GuestAuthorizenetCheckoutSection.city}}" userInput="{{customerAddress.city}}" stepKey="fillCity"/> + <selectOption selector="{{GuestAuthorizenetCheckoutSection.state}}" userInput="{{customerAddress.state}}" stepKey="selectState"/> + <fillField selector="{{GuestAuthorizenetCheckoutSection.postcode}}" userInput="{{customerAddress.postcode}}" stepKey="fillPostCode"/> + <fillField selector="{{GuestAuthorizenetCheckoutSection.telephone}}" userInput="{{customerAddress.telephone}}" stepKey="fillTelephone"/> + <click selector="{{GuestAuthorizenetCheckoutSection.update}}" stepKey="updateAddress"/> + <waitForPageLoad stepKey="waitForUpdate"/> + </actionGroup> +</actionGroups> 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 @@ +<?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="ViewAndValidateOrderActionGroup"> + <arguments> + <argument name="amount" type="string"/> + <argument name="status" type="string"/> + <argument name="captureStatus" type="string"/> + <argument name="closedStatus" type="string"/> + </arguments> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> + <click selector="{{AdminMenuSection.sales}}" stepKey="clickSales"/> + <waitForPageLoad stepKey="waitForSalesSubsection"/> + <click selector="{{AdminMenuSection.orders}}" stepKey="clickOrders"/> + <waitForPageLoad stepKey="waitForOrdersGrid" time="30"/> + <click selector="{{OrdersGridSection.viewMostRecentOrder}}" stepKey="viewOrder"/> + <waitForPageLoad stepKey="waitForViewOrder"/> + <click selector="{{ViewOrderSection.openInvoiceForm}}" stepKey="openInvoiceForm"/> + <selectOption selector="{{ViewOrderSection.selectCaptureType}}" stepKey="selectCaptureType" userInput="Capture Online"/> + <click selector="{{ViewOrderSection.submitInvoice}}" stepKey="submitInvoice"/> + <waitForPageLoad stepKey="waitForInvoiceLoad"/> + <click selector="{{ViewOrderSection.commentsHistory}}" stepKey="viewCommentsHistory"/> + <waitForPageLoad stepKey="waitForHistoryLoad"/> + <see userInput="{{amount}}" selector="{{ViewOrderSection.capturedAmountText}}" stepKey="validateCapturedAmount"/> + <see userInput="{{status}}" selector="{{ViewOrderSection.orderStatus}}" stepKey="validateOrderStatus"/> + <click selector="{{ViewOrderSection.invoices}}" stepKey="openInvoices"/> + <waitForPageLoad stepKey="waitForInvoices"/> + <seeElement selector="{{ViewOrderSection.firstInvoice}}" stepKey="seeFirstInvoice"/> + <click selector="{{ViewOrderSection.transactions}}" stepKey="openTransactions"/> + <waitForPageLoad stepKey="waitForTransactions"/> + <see userInput="{{captureStatus}}" selector="{{ViewOrderSection.confirmCapture}}" stepKey="seeCapture"/> + <!-- Enable below line after fix of MC- + <see userInput="{{closedStatus}}" selector="{{ViewOrderSection.confirmClosed}}" stepKey="seeClosed"/> + --> + </actionGroup> + <actionGroup name="ViewAndValidateOrderActionGroupNoSubmit"> + <arguments> + <argument name="amount" type="string"/> + <argument name="status" type="string"/> + <argument name="captureStatus" type="string"/> + <argument name="closedStatus" type="string"/> + </arguments> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> + <click selector="{{AdminMenuSection.sales}}" stepKey="clickSales"/> + <waitForPageLoad stepKey="waitForSalesSubsection"/> + <click selector="{{AdminMenuSection.orders}}" stepKey="clickOrders"/> + <waitForPageLoad stepKey="waitForOrdersGrid" time="30"/> + <click selector="{{OrdersGridSection.viewMostRecentOrder}}" stepKey="viewOrder"/> + <waitForPageLoad stepKey="waitForViewOrder"/> + <click selector="{{ViewOrderSection.commentsHistory}}" stepKey="viewCommentsHistory"/> + <waitForPageLoad stepKey="waitForHistoryLoad"/> + <see userInput="{{amount}}" selector="{{ViewOrderSection.capturedAmountTextUnsubmitted}}" stepKey="validateCapturedAmount"/> + <see userInput="{{status}}" selector="{{ViewOrderSection.orderStatus}}" stepKey="validateOrderStatus"/> + <click selector="{{ViewOrderSection.invoices}}" stepKey="openInvoices"/> + <waitForPageLoad stepKey="waitForInvoices"/> + <seeElement selector="{{ViewOrderSection.firstInvoice}}" stepKey="seeFirstInvoice"/> + <click selector="{{ViewOrderSection.transactions}}" stepKey="openTransactions"/> + <waitForPageLoad stepKey="waitForTransactions"/> + <see userInput="{{captureStatus}}" selector="{{ViewOrderSection.confirmCapture}}" stepKey="seeCapture"/> + <!-- Enable below line after fix of MC- + <see userInput="{{closedStatus}}" selector="{{ViewOrderSection.confirmClosed}}" stepKey="seeClosed"/> + --> + </actionGroup> +</actionGroups> 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 @@ +<?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="AuthorizenetAcceptjsOrderValidationData" type="AuthorizenetAcceptjsCredentials"> + <data key="virtualProductOrderAmount">$24.68</data> + <data key="twoSimpleProductsOrderAmount">$128.00</data> + <data key="processingStatusProcessing">Processing</data> + <data key="captureStatusCapture">Capture</data> + <data key="closedStatusNo">No</data> + </entity> +</entities> 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 <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/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 <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/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 @@ +<?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="AdminMenuSection"> + <element name="dashboard" type="button" selector="#menu-magento-backend-dashboard"/> + <element name="sales" type="button" selector="#menu-magento-sales-sales"/> + <element name="orders" type="button" selector="//*[@id='menu-magento-sales-sales']//span[text()='Orders']"/> + <element name="catalog" type="button" selector="#menu-magento-catalog-catalog"/> + <element name="customers" type="button" selector="#menu-magento-customer-customer"/> + <element name="marketing" type="button" selector="#menu-magento-backend-marketing"/> + <element name="content" type="button" selector="#menu-magento-backend-content"/> + <element name="reports" type="button" selector="#menu-magento-reports-report"/> + <element name="stores" type="button" selector="#menu-magento-backend-stores"/> + <element name="system" type="button" selector="#menu-magento-backend-system"/> + <element name="findPartners" type="button" selector="#menu-magento-marketplace-partners"/> + <element name="currencySetup" type="button" selector="//a[contains(@class, 'item-nav')]//span[text()='Currency Setup']"/> + </section> +</sections> 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 @@ +<?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="AuthorizenetAcceptjsConfigurationSection"> + <element name="openSectionToggle" type="button" selector="#payment_us_authorizenet_acceptjs-head"/> + <element name="alreadyOpenSectionToggle" type="button" selector="#payment_us_authorizenet_acceptjs-head.open"/> + <element name="apiLoginIdField" type="input" selector="#payment_us_authorizenet_acceptjs_required_login"/> + <element name="transactionKeyField" type="input" selector="#payment_us_authorizenet_acceptjs_required_trans_key"/> + <element name="publicClientKeyField" type="input" selector="#payment_us_authorizenet_acceptjs_required_public_client_key"/> + <element name="signatureKeyField" type="input" selector="#payment_us_authorizenet_acceptjs_required_trans_signature_key"/> + <element name="enabledDefaultCheckbox" type="input" selector="#payment_us_authorizenet_acceptjs_active_inherit"/> + <element name="enabledDefaultSelect" type="select" selector="#payment_us_authorizenet_acceptjs_active"/> + <element name="paymentActionCheckbox" type="input" selector="#payment_us_authorizenet_acceptjs_required_payment_action_inherit"/> + <element name="paymentActionSelect" type="select" selector="#payment_us_authorizenet_acceptjs_required_payment_action"/> + <element name="paymentActionSelectDisabled" type="select" selector="#payment_us_authorizenet_acceptjs_required_payment_action[disabled='disabled']"/> + </section> +</sections> 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 @@ +<?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="AuthorizenetCheckoutSection"> + <element name="selectAuthorizenet" type="button" selector="#authorizenet_acceptjs"/> + <element name="cardInput" type="input" selector="#authorizenet_acceptjs_cc_number"/> + <element name="expMonth" type="select" selector="#authorizenet_acceptjs_expiration"/> + <element name="expYear" type="select" selector="#authorizenet_acceptjs_expiration_yr"/> + <element name="cvv" type="input" selector="#authorizenet_acceptjs_cc_cid"/> + <element name="checkoutButton" type="button" selector="._active button.action.checkout"/> + </section> +</sections> 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 @@ +<?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="ConfigurationMainActionsSection"> + <element name="save" type="button" selector="#save"/> + </section> +</sections> 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 @@ +<?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="GuestAuthorizenetCheckoutSection"> + <element name="firstName" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.firstname'] input"/> + <element name="lastName" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.lastname'] input"/> + <element name="street" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.street.0'] input"/> + <element name="city" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.city'] input"/> + <element name="state" type="select" selector="div[name='billingAddressauthorizenet_acceptjs.region_id'] select"/> + <element name="postcode" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.postcode'] input"/> + <element name="country" type="select" selector="div[name='billingAddressauthorizenet_acceptjs.country_id'] select"/> + <element name="telephone" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.telephone'] input"/> + <element name="update" type="button" selector=".payment-method._active button.action-update" /> + </section> +</sections> 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 @@ +<?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="OrdersGridSection"> + <element name="viewMostRecentOrder" type="button" selector="#container div.admin__data-grid-wrap td:nth-of-type(10) a"/> + </section> +</sections> 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 @@ +<?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="StoresConfigurationListSection"> + <element name="sales" type="button" selector="#system_config_tabs > div:nth-child(4) > div"/> + <element name="salesPaymentMethods" type="button" selector="//a[contains(@class, 'item-nav')]//span[text()='Payment Methods']"/> + </section> +</sections> 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 @@ +<?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="StoresSubmenuSection"> + <element name="configuration" type="button" selector="#menu-magento-backend-stores li[data-ui-id='menu-magento-config-system-config']"/> + </section> +</sections> 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 @@ +<?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="ViewOrderSection"> + <element name="openInvoiceForm" type="button" selector="#order_invoice"/> + <element name="selectCaptureType" type="select" selector="select[name='invoice[capture_case]']"/> + <element name="submitInvoice" type="button" selector="button.submit-button"/> + <element name="commentsHistory" type="button" selector="#sales_order_view_tabs_order_history"/> + <element name="capturedAmountText" type="text" selector="//div[@class='comments-block-item'][2]/div[@class='comments-block-item-comment']"/> + <element name="capturedAmountTextUnsubmitted" type="text" selector="//div[@class='comments-block-item'][1]/div[@class='comments-block-item-comment']"/> + <element name="orderStatus" type="text" selector=".note-list-item .note-list-status"/> + <element name="invoices" type="button" selector="#sales_order_view_tabs_order_invoices"/> + <element name="firstInvoice" type="text" selector=".data-grid tbody tr"/> + <element name="transactions" type="button" selector="#sales_order_view_tabs_order_transactions"/> + <element name="confirmCapture" type="text" selector="//table[@id='order_transactions_table']/tbody/tr/td[6]"/> + <element name="confirmClosed" type="text" selector="//table[@id='order_transactions_table']/tbody/tr/td[7]"/> + </section> +</sections> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="FullCaptureAuthorizenetAcceptjsTest"> + <annotations> + <stories value="Authorize.net Accept.js"/> + <title value="Full Capture using Authorize.net Accept.js"/> + <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..cef7883bd5dbc --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionResponseValidatorTest.php @@ -0,0 +1,213 @@ +<?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; + +class TransactionResponseValidatorTest extends TestCase +{ + private const RESPONSE_CODE_APPROVED = 1; + private const RESPONSE_CODE_HELD = 4; + private const RESPONSE_REASON_CODE_APPROVED = 1; + private const RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED = 252; + private const RESPONSE_REASON_CODE_PENDING_REVIEW = 253; + + /** + * @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 [ + // This validator only cares about successful edge cases so test for default behavior + [ + [ + 'responseCode' => 'foo', + ], + true, + [], + [] + ], + + // 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'] + ], + ]; + } +} 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)</title> + <cvv_enabled>1</cvv_enabled> + <public_client_key/> + <trans_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <trans_md5 backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <trans_signature_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <allowspecific>0</allowspecific> + <currency>USD</currency> + <environment>production</environment> + <privateInfoKeys>authCode,avsResultCode,cvvResultCode,cavvResultCode</privateInfoKeys> + <paymentInfoKeys>accountType,ccLast4,authCode,avsResultCode,cvvResultCode,cavvResultCode</paymentInfoKeys> + <transactionSyncKeys>transactionStatus,responseCode,responseReasonCode,authCode,AVSResponse,cardCodeResponse,CAVVResponse</transactionSyncKeys> + </authorizenet_acceptjs> + </payment> + </default> +</config> 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 @@ +<?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\Gateway\Config"> + <arguments> + <argument name="methodCode" xsi:type="const">Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD</argument> + </arguments> + </type> + <virtualType name="AuthorizenetAcceptjsFacade" type="Magento\Payment\Model\Method\Adapter"> + <arguments> + <argument name="code" xsi:type="string">authorizenet_acceptjs</argument> + <argument name="formBlockType" xsi:type="string">Magento\AuthorizenetAcceptjs\Block\Form</argument> + <argument name="infoBlockType" xsi:type="string">AuthorizenetAcceptjsInfoBlock</argument> + <argument name="valueHandlerPool" xsi:type="object">AuthorizenetAcceptjsValueHandlerPool</argument> + <argument name="validatorPool" xsi:type="object">AuthorizenetAcceptjsValidatorPool</argument> + <argument name="commandPool" xsi:type="object">AuthorizenetAcceptjsCommandPool</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsCommandPool" type="Magento\Payment\Gateway\Command\CommandPool"> + <arguments> + <argument name="commands" xsi:type="array"> + <item name="authorize" xsi:type="string">AuthorizenetAcceptjsAuthorizeCommand</item> + <item name="capture" xsi:type="string">AuthorizenetAcceptjsCaptureCommand</item> + <item name="sale" xsi:type="string">AuthorizenetAcceptjsSaleCommand</item> + <item name="settle" xsi:type="string">AuthorizenetAcceptjsSettleCommand</item> + <item name="void" xsi:type="string">AuthorizenetAcceptjsVoidCommand</item> + <item name="refund" xsi:type="string">AuthorizenetAcceptjsRefundCommand</item> + <item name="refund_settled" xsi:type="string">AuthorizenetAcceptjsRefundSettledCommand</item> + <item name="cancel" xsi:type="string">AuthorizenetAcceptjsCancelCommand</item> + <item name="accept_payment" xsi:type="string">AuthorizenetAcceptjsAcceptPaymentCommand</item> + <item name="accept_fds" xsi:type="string">AuthorizenetAcceptjsAcceptFdsCommand</item> + <item name="deny_payment" xsi:type="string">AuthorizenetAcceptjsCancelCommand</item> + <item name="get_transaction_details" xsi:type="string">AuthorizenetAcceptjsTransactionDetailsCommand</item> + <item name="fetch_transaction_information" xsi:type="string">AuthorizenetAcceptjsFetchTransactionInfoCommand</item> + </argument> + </arguments> + </virtualType> + + <!-- Validators --> + <virtualType name="AuthorizenetAcceptjsAcceptsFdsRequestValidator" type="Magento\Payment\Gateway\Validator\ValidatorComposite"> + <arguments> + <argument name="validators" xsi:type="array"> + <item name="general" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsTransactionValidator" type="Magento\Payment\Gateway\Validator\ValidatorComposite"> + <arguments> + <argument name="chainBreakingValidators" xsi:type="array"> + <item name="general" xsi:type="boolean">true</item> + </argument> + <argument name="validators" xsi:type="array"> + <item name="general" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator</item> + <item name="transaction_status" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionResponseValidator</item> + <item name="transaction_hash" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionHashValidator</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsTransactionDetailsValidator" type="Magento\Payment\Gateway\Validator\ValidatorComposite"> + <arguments> + <argument name="validators" xsi:type="array"> + <item name="general" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsCountryValidator" type="Magento\Payment\Gateway\Validator\CountryValidator"> + <arguments> + <argument name="config" xsi:type="object">Magento\AuthorizenetAcceptjs\Gateway\Config</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsValidatorPool" type="Magento\Payment\Gateway\Validator\ValidatorPool"> + <arguments> + <argument name="validators" xsi:type="array"> + <item name="country" xsi:type="string">AuthorizenetAcceptjsCountryValidator</item> + </argument> + </arguments> + </virtualType> + <!-- End Validators --> + + <!-- Gateway Commands --> + <virtualType name="AuthorizenetAcceptjsFetchTransactionInfoCommand" type="Magento\AuthorizenetAcceptjs\Gateway\Command\FetchTransactionInfoCommand"> + <arguments> + <argument name="commandPool" xsi:type="object">AuthorizenetAcceptjsCommandPool</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsTransactionDetailsCommand" type="Magento\AuthorizenetAcceptjs\Gateway\Command\GatewayQueryCommand"> + <arguments> + <argument name="requestBuilder" xsi:type="object">AuthorizenetAcceptjsTransactionDetailsRequest</argument> + <argument name="transferFactory" xsi:type="object">AuthorizenetAcceptjsDefaultTransferFactory</argument> + <argument name="client" xsi:type="object">Magento\AuthorizenetAcceptjs\Gateway\Http\Client</argument> + <argument name="validator" xsi:type="object">AuthorizenetAcceptjsTransactionDetailsValidator</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsAuthorizeCommand" type="Magento\Payment\Gateway\Command\GatewayCommand"> + <arguments> + <argument name="requestBuilder" xsi:type="object">AuthorizenetAcceptjsAuthorizeRequest</argument> + <argument name="transferFactory" xsi:type="object">AuthorizenetAcceptjsDefaultTransferFactory</argument> + <argument name="client" xsi:type="object">Magento\AuthorizenetAcceptjs\Gateway\Http\Client</argument> + <argument name="handler" xsi:type="object">AuthorizenetAcceptjsAuthorizationHandler</argument> + <argument name="validator" xsi:type="object">AuthorizenetAcceptjsTransactionValidator</argument> + <argument name="errorMessageMapper" xsi:type="object">AuthorizenetAcceptjsVirtualErrorMessageMapper</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsAcceptFdsCommand" type="Magento\AuthorizenetAcceptjs\Gateway\Command\GatewayQueryCommand"> + <arguments> + <argument name="requestBuilder" xsi:type="object">AuthorizenetAcceptjsAcceptsFdsRequest</argument> + <argument name="transferFactory" xsi:type="object">AuthorizenetAcceptjsDefaultTransferFactory</argument> + <argument name="client" xsi:type="object">Magento\AuthorizenetAcceptjs\Gateway\Http\Client</argument> + <argument name="validator" xsi:type="object">AuthorizenetAcceptjsAcceptsFdsRequestValidator</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsAcceptPaymentCommand" type="Magento\AuthorizenetAcceptjs\Gateway\Command\AcceptPaymentStrategyCommand"> + <arguments> + <argument name="commandPool" xsi:type="object">AuthorizenetAcceptjsCommandPool</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsSaleCommand" type="AuthorizenetAcceptjsAuthorizeCommand"> + <arguments> + <argument name="requestBuilder" xsi:type="object">AuthorizenetAcceptjsSaleRequest</argument> + <argument name="handler" xsi:type="object">AuthorizenetAcceptjsSaleHandler</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsRefundCommand" type="Magento\AuthorizenetAcceptjs\Gateway\Command\RefundTransactionStrategyCommand"> + <arguments> + <argument name="commandPool" xsi:type="object">AuthorizenetAcceptjsCommandPool</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsRefundSettledCommand" type="AuthorizenetAcceptjsAuthorizeCommand"> + <arguments> + <argument name="requestBuilder" xsi:type="object">AuthorizenetAcceptjsRefundRequest</argument> + <argument name="handler" xsi:type="object">AuthorizenetAcceptjsRefundSettledHandler</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsCaptureCommand" type="Magento\AuthorizenetAcceptjs\Gateway\Command\CaptureStrategyCommand"> + <arguments> + <argument name="commandPool" xsi:type="object">AuthorizenetAcceptjsCommandPool</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsSettleCommand" type="AuthorizenetAcceptjsAuthorizeCommand"> + <arguments> + <argument name="requestBuilder" xsi:type="object">AuthorizenetAcceptjsCaptureRequest</argument> + <argument name="handler" xsi:type="object">AuthorizenetAcceptjsCaptureTransactionHandler</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsVoidCommand" type="Magento\Payment\Gateway\Command\GatewayCommand"> + <arguments> + <argument name="requestBuilder" xsi:type="object">AuthorizenetAcceptjsVoidRequest</argument> + <argument name="transferFactory" xsi:type="object">AuthorizenetAcceptjsDefaultTransferFactory</argument> + <argument name="client" xsi:type="object">Magento\AuthorizenetAcceptjs\Gateway\Http\Client</argument> + <argument name="handler" xsi:type="object">AuthorizenetAcceptjsVoidHandler</argument> + <argument name="validator" xsi:type="object">AuthorizenetAcceptjsTransactionValidator</argument> + <argument name="errorMessageMapper" xsi:type="object">AuthorizenetAcceptjsVirtualErrorMessageMapper</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsCancelCommand" type="AuthorizenetAcceptjsVoidCommand"> + <arguments> + <argument name="handler" xsi:type="object">AuthorizenetAcceptjsCancelHandler</argument> + </arguments> + </virtualType> + <!-- End Gateway Commands --> + + <!-- Handlers --> + <virtualType name="AuthorizenetAcceptjsPaymentReviewStatusHandler" type="Magento\Payment\Gateway\Response\HandlerChain"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="payment_status" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\PaymentReviewStatusHandler</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsAuthorizationHandler" type="Magento\Payment\Gateway\Response\HandlerChain"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="transaction_id" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionIdHandler</item> + <item name="payment_details" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\PaymentResponseHandler</item> + <item name="transaction_details" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionDetailsResponseHandler</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsSaleHandler" type="AuthorizenetAcceptjsAuthorizationHandler"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="close_parent_transaction" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsCaptureTransactionHandler" type="Magento\Payment\Gateway\Response\HandlerChain"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="close_parent_transaction" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsRefundSettledHandler" type="Magento\Payment\Gateway\Response\HandlerChain"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="transaction_id" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionIdHandler</item> + <item name="close_parent_transaction" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler</item> + <item name="close_transaction" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\CloseTransactionHandler</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsVoidHandler" type="Magento\Payment\Gateway\Response\HandlerChain"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="void" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\VoidResponseHandler</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsCancelHandler" type="Magento\Payment\Gateway\Response\HandlerChain"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="close_transaction" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\CloseTransactionHandler</item> + <item name="close_parent_transaction" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler</item> + </argument> + </arguments> + </virtualType> + <!-- End Handlers --> + + <!-- Request Types --> + <!-- Be advised that the order of the request parameters is important --> + <virtualType name="AuthorizenetAcceptjsTransactionDetailsRequest" type="Magento\Payment\Gateway\Request\BuilderComposite"> + <arguments> + <argument name="builders" xsi:type="array"> + <item name="request_type" xsi:type="string">AuthorizenetAcceptjsTransactionDetailsRequestTypeBuilder</item> + <item name="store" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder</item> + <item name="merchant_account" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder</item> + <item name="reference_transaction" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\TransactionDetailsDataBuilder</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsAcceptsFdsRequest" type="Magento\Payment\Gateway\Request\BuilderComposite"> + <arguments> + <argument name="builders" xsi:type="array"> + <item name="request_type" xsi:type="string">AuthorizenetAcceptjsAcceptsFdsRequestTypeBuilder</item> + <item name="store" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder</item> + <item name="merchant_account" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder</item> + <item name="reference_transaction" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AcceptFdsDataBuilder</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsAuthorizeRequest" type="Magento\Payment\Gateway\Request\BuilderComposite"> + <arguments> + <argument name="builders" xsi:type="array"> + <item name="request_type" xsi:type="string">AuthorizenetAcceptjsTransactionRequestTypeBuilder</item> + <item name="store" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder</item> + <item name="merchant_account" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder</item> + <item name="transaction_type" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AuthorizeDataBuilder</item> + <item name="amount" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AmountDataBuilder</item> + <item name="payment" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PaymentDataBuilder</item> + <item name="shipping" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\ShippingDataBuilder</item> + <item name="solution" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\SolutionDataBuilder</item> + <item name="order" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\OrderDataBuilder</item> + <item name="po" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PoDataBuilder</item> + <item name="customer" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\CustomerDataBuilder</item> + <item name="address" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AddressDataBuilder</item> + <item name="custom_settings" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\CustomSettingsBuilder</item> + <item name="passthrough_data" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsSaleRequest" type="AuthorizenetAcceptjsAuthorizeRequest"> + <arguments> + <argument name="builders" xsi:type="array"> + <item name="transaction_type" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\SaleDataBuilder</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsRefundRequest" type="Magento\Payment\Gateway\Request\BuilderComposite"> + <arguments> + <argument name="builders" xsi:type="array"> + <item name="request_type" xsi:type="string">AuthorizenetAcceptjsTransactionRequestTypeBuilder</item> + <item name="store" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder</item> + <item name="merchant_account" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder</item> + <item name="transaction_type" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\RefundTransactionTypeDataBuilder</item> + <item name="amount" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AmountDataBuilder</item> + <item name="payment" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\RefundPaymentDataBuilder</item> + <item name="shipping" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\ShippingDataBuilder</item> + <item name="ref_transaction" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\RefundReferenceTransactionDataBuilder</item> + <item name="order" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\OrderDataBuilder</item> + <item name="po" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PoDataBuilder</item> + <item name="customer" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\CustomerDataBuilder</item> + <item name="address" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AddressDataBuilder</item> + <item name="custom_settings" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\CustomSettingsBuilder</item> + <item name="passthrough_data" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsCaptureRequest" type="Magento\Payment\Gateway\Request\BuilderComposite"> + <arguments> + <argument name="builders" xsi:type="array"> + <item name="request_type" xsi:type="string">AuthorizenetAcceptjsTransactionRequestTypeBuilder</item> + <item name="store" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder</item> + <item name="merchant_account" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder</item> + <item name="transaction_type" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\CaptureDataBuilder</item> + <item name="passthrough_data" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsVoidRequest" type="Magento\Payment\Gateway\Request\BuilderComposite"> + <arguments> + <argument name="builders" xsi:type="array"> + <item name="request_type" xsi:type="string">AuthorizenetAcceptjsTransactionRequestTypeBuilder</item> + <item name="store" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder</item> + <item name="merchant_account" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder</item> + <item name="void" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\VoidDataBuilder</item> + <item name="passthrough_data" xsi:type="string">Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder</item> + </argument> + </arguments> + </virtualType> + <!-- End Request Types --> + + <!-- Request Payload Types --> + <virtualType name="AuthorizenetAcceptjsTransactionRequestTypeBuilder" type="Magento\AuthorizenetAcceptjs\Gateway\Request\RequestTypeBuilder"> + <arguments> + <argument name="type" xsi:type="string">createTransactionRequest</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsTransactionDetailsRequestTypeBuilder" type="Magento\AuthorizenetAcceptjs\Gateway\Request\RequestTypeBuilder"> + <arguments> + <argument name="type" xsi:type="string">getTransactionDetailsRequest</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsAcceptsFdsRequestTypeBuilder" type="Magento\AuthorizenetAcceptjs\Gateway\Request\RequestTypeBuilder"> + <arguments> + <argument name="type" xsi:type="string">updateHeldTransactionRequest</argument> + </arguments> + </virtualType> + <!-- End Request Payload Types --> + + <!-- Error Mapping --> + <virtualType name="AuthorizenetAcceptjsErrorMappingConfigReader" type="Magento\Payment\Gateway\ErrorMapper\VirtualConfigReader"> + <arguments> + <argument name="fileName" xsi:type="string">authorizenet_acceptjs_error_mapping.xml</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsErrorMappingData" type="Magento\Payment\Gateway\ErrorMapper\MappingData"> + <arguments> + <argument name="reader" xsi:type="object">AuthorizenetAcceptjsErrorMappingConfigReader</argument> + <argument name="cacheId" xsi:type="string">authorizenet_acceptjs_error_mapper</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsVirtualErrorMessageMapper" type="Magento\Payment\Gateway\ErrorMapper\ErrorMessageMapper"> + <arguments> + <argument name="messageMapping" xsi:type="object">AuthorizenetAcceptjsErrorMappingData</argument> + </arguments> + </virtualType> + <!-- End Error Mapping --> + + <type name="Magento\AuthorizenetAcceptjs\Gateway\Command\FetchTransactionInfoCommand"> + <arguments> + <argument name="handler" xsi:type="object">AuthorizenetAcceptjsPaymentReviewStatusHandler</argument> + </arguments> + </type> + <type name="Magento\Payment\Gateway\Command\CommandManagerPool"> + <arguments> + <argument name="executors" xsi:type="array"> + <item name="authorizenet_acceptjs" xsi:type="string">AuthorizenetAcceptjsCommandManager</item> + </argument> + </arguments> + </type> + <type name="Magento\Config\Model\Config\TypePool"> + <arguments> + <argument name="sensitive" xsi:type="array"> + <item name="payment/authorizenet_acceptjs/login" xsi:type="string">1</item> + <item name="payment/authorizenet_acceptjs/trans_key" xsi:type="string">1</item> + <item name="payment/authorizenet_acceptjs/signature_key" xsi:type="string">1</item> + <item name="payment/authorizenet_acceptjs/trans_md5" xsi:type="string">1</item> + <item name="payment/authorizenet_acceptjs/api_url" xsi:type="string">1</item> + <item name="payment/authorizenet_acceptjs/email_customer" xsi:type="string">1</item> + </argument> + <argument name="environment" xsi:type="array"> + <item name="payment/authorizenet_acceptjs/api_url" xsi:type="string">1</item> + <item name="payment/authorizenet_acceptjs/test" xsi:type="string">1</item> + </argument> + </arguments> + </type> + <virtualType name="AuthorizenetAcceptjsDefaultValueHandler" type="Magento\Payment\Gateway\Config\ConfigValueHandler"> + <arguments> + <argument name="configInterface" xsi:type="object">Magento\AuthorizenetAcceptjs\Gateway\Config</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsValueHandlerPool" type="Magento\Payment\Gateway\Config\ValueHandlerPool"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="default" xsi:type="string">AuthorizenetAcceptjsDefaultValueHandler</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsCommandManager" type="Magento\Payment\Gateway\Command\CommandManager"> + <arguments> + <argument name="commandPool" xsi:type="object">AuthorizenetAcceptjsCommandPool</argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsLogger" type="Magento\Payment\Model\Method\Logger"> + <arguments> + <argument name="config" xsi:type="object">Magento\AuthorizenetAcceptjs\Gateway\Config</argument> + </arguments> + </virtualType> + <type name="Magento\AuthorizenetAcceptjs\Gateway\Http\Client"> + <arguments> + <argument name="paymentLogger" xsi:type="object">AuthorizenetAcceptjsLogger</argument> + </arguments> + </type> + <virtualType name="AuthorizenetAcceptjsRemoveStoreConfigFilter" type="Magento\AuthorizenetAcceptjs\Gateway\Http\Payload\Filter\RemoveFieldsFilter"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="0" xsi:type="string">store_id</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsDefaultTransferFactory" type="Magento\AuthorizenetAcceptjs\Gateway\Http\TransferFactory"> + <arguments> + <argument name="payloadFilters" xsi:type="array"> + <item name="store_config" xsi:type="object">AuthorizenetAcceptjsRemoveStoreConfigFilter</item> + </argument> + </arguments> + </virtualType> + <virtualType name="AuthorizenetAcceptjsInfoBlock" type="Magento\AuthorizenetAcceptjs\Block\Info"> + <arguments> + <argument name="config" xsi:type="object">Magento\AuthorizenetAcceptjs\Gateway\Config</argument> + </arguments> + </virtualType> +</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 @@ +<?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="payment_method_assign_data_authorizenet_acceptjs"> + <observer name="authorizenet_acceptjs_gateway_data_assign" instance="Magento\AuthorizenetAcceptjs\Observer\DataAssignObserver" /> + </event> +</config> 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 @@ +<?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\Info"> + <arguments> + <argument name="data" xsi:type="array"> + <item xsi:type="string" name="is_secure_mode">1</item> + </argument> + </arguments> + </type> + <type name="Magento\Checkout\Model\CompositeConfigProvider"> + <arguments> + <argument name="configProviders" xsi:type="array"> + <item name="authorizenet_acceptjs_config_provider" xsi:type="object">Magento\AuthorizenetAcceptjs\Model\Ui\ConfigProvider</item> + </argument> + </arguments> + </type> + <type name="Magento\Payment\Model\CcGenericConfigProvider"> + <arguments> + <argument name="methodCodes" xsi:type="array"> + <item name="authorizenet_acceptjs" xsi:type="const">Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD</item> + </argument> + </arguments> + </type> +</config> 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 @@ +<?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_AuthorizenetAcceptjs" setup_version="1.0.0"> + <sequence> + <module name="Magento_Customer"/> + <module name="Magento_Payment"/> + <module name="Magento_Checkout"/> + <module name="Magento_Catalog"/> + <module name="Magento_Quote"/> + <module name="Magento_Theme"/> + </sequence> + </module> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<payment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/payment.xsd"> + <methods> + <method name="authorizenet_acceptjs"> + <allow_multiple_address>0</allow_multiple_address> + </method> + </methods> +</payment> 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 @@ +<?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_AuthorizenetAcceptjs', __DIR__); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/layout/sales_order_create_index.xml b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/layout/sales_order_create_index.xml new file mode 100644 index 0000000000000..ee1de3ed7efae --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/layout/sales_order_create_index.xml @@ -0,0 +1,24 @@ +<?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="order_create_billing_form"> + <action method="setMethodFormTemplate"> + <argument name="method" xsi:type="const">Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD</argument> + <argument name="template" xsi:type="string">Magento_AuthorizenetAcceptjs::form/cc.phtml</argument> + </action> + </referenceBlock> + <referenceBlock name="content"> + <block name="authorizenet_acceptjs_payment_script" + as="authorizenet_acceptjs_payment_script" + template="Magento_AuthorizenetAcceptjs::payment/script.phtml" + class="Magento\AuthorizenetAcceptjs\Block\Payment" + after="billing_method"/> + </referenceBlock> + </body> +</page> 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 @@ +<?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="order.create.billing.method.form"> + <action method="setMethodFormTemplate"> + <argument name="method" xsi:type="const">Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD</argument> + <argument name="template" xsi:type="string">Magento_AuthorizenetAcceptjs::form/cc.phtml</argument> + </action> + </referenceBlock> + </body> +</page> 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile +/** + * @var Magento\AuthorizenetAcceptjs\Block\Form $block + */ +$code = $block->escapeHtml($block->getMethodCode()); +$ccType = $block->getInfoData('cc_type'); +$ccExpMonth = $block->getInfoData('cc_exp_month'); +$ccExpYear = $block->getInfoData('cc_exp_year'); +?> +<fieldset class="admin__fieldset payment-method" id="payment_form_<?= /* @noEscape */ $code ?>" + style="display:none"> + <div class="field-type admin__field _required"> + <label class="admin__field-label" for="<?= /* @noEscape */ $code ?>_cc_type"> + <span><?= $block->escapeHtml(__('Credit Card Type')) ?></span> + </label> + <div class="admin__field-control"> + <select id="<?= /* @noEscape */ $code ?>_cc_type" name="payment[cc_type]" + class="required-entry validate-cc-type-select admin__control-select"> + <option value=""></option> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> + <option value="<?= $block->escapeHtml($typeCode) ?>" <?php if ($typeCode == $ccType): ?>selected="selected"<?php endif ?>> + <?= $block->escapeHtml($typeName) ?> + </option> + <?php endforeach ?> + </select> + </div> + </div> + <div class="field-number admin__field _required"> + <label class="admin__field-label" for="<?= /* @noEscape */ $code ?>_cc_number"> + <span><?= $block->escapeHtml(__('Credit Card Number')) ?></span> + </label> + <div class="admin__field-control"> + <input type="text" id="<?= /* @noEscape */ $code ?>_cc_number" name="payment[cc_number]" + title="<?= $block->escapeHtml(__('Credit Card Number')) ?>" class="admin__control-text validate-cc-number" + value="<?= /* @noEscape */ $block->getInfoData('cc_number') ?>"/> + </div> + </div> + <div class="field-date admin__field _required"> + <label class="admin__field-label" for="<?= /* @noEscape */ $code ?>_cc_exp_month"> + <span><?= $block->escapeHtml(__('Expiration Date')) ?></span> + </label> + <div class="admin__field-control"> + <select id="<?= /* @noEscape */ $code ?>_cc_exp_month" name="payment[cc_exp_month]" + class="admin__control-select admin__control-select-month validate-cc-exp required-entry"> + <?php foreach ($block->getCcMonths() as $k => $v): ?> + <option value="<?= $block->escapeHtml($k) ?>" + <?php if ($k == $ccExpMonth): ?>selected="selected"<?php endif ?>> + <?= $block->escapeHtml($v) ?> + </option> + <?php endforeach; ?> + </select> + <select id="<?= /* @noEscape */ $code ?>_cc_exp_year" name="payment[cc_exp_year]" + class="admin__control-select admin__control-select-year required-entry"> + <?php foreach ($block->getCcYears() as $k => $v): ?> + <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" + <?php if ($k == $ccExpYear): ?>selected="selected"<?php endif ?>> + <?= $block->escapeHtml($v) ?> + </option> + <?php endforeach ?> + </select> + </div> + </div> + + <?php if ($block->isCvvEnabled()): ?> + <div class="field-number required admin__field _required"> + <label class="admin__field-label" for="<?= /* @noEscape */ $code ?>_cc_cid"> + <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> + </label> + <div class="admin__field-control"> + <input type="text" title="<?= $block->escapeHtml(__('Card Verification Number')) ?>" + class="required-entry validate-cc-cvn admin__control-cvn admin__control-text" + id="<?= /* @noEscape */ $code ?>_cc_cid" + name="payment[cc_cid]" value="<?= /* @noEscape */ $block->getInfoData('cc_cid') ?>"/> + </div> + </div> + <?php endif; ?> + + <input type="hidden" id="<?= /* @noEscape */ $code ?>_opaque_data_descriptor" + name="payment[opaqueDataDescriptor]" + value="<?=$block->escapeHtmlAttr($block->getInfoData('opaqueDataDescriptor')) ?>"/> + <input type="hidden" id="<?= /* @noEscape */ $code ?>_opaque_data_value" + name="payment[opaqueDataValue]" + value="<?=$block->escapeHtmlAttr($block->getInfoData('opaqueDataValue')) ?>"/> + <input type="hidden" id="<?= /* @noEscape */ $code ?>_cc_last_4" + name="payment[ccLast4]" + value="<?=$block->escapeHtmlAttr($block->getInfoData('ccLast4')) ?>"/> +</fieldset> 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var Magento\AuthorizenetAcceptjs\Block\Payment $block */ +?> +<script type="text/x-magento-init"> + { + "#payment_form_<?= $block->escapeJs($block->escapeHtml($block->getMethodCode())) ?>": { + "Magento_AuthorizenetAcceptjs/js/payment-form": { + "config": <?= /* @noEscape */ $block->getPaymentConfig() ?> + } + } + } +</script> \ 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 @@ +<?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="component" xsi:type="string">uiComponent</item> + <item name="children" xsi:type="array"> + <item name="payment" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="renders" xsi:type="array"> + <!-- merge payment method renders here --> + <item name="children" xsi:type="array"> + <item name="authorizenet_acceptjs" xsi:type="array"> + <item name="component" xsi:type="string">Magento_AuthorizenetAcceptjs/js/view/payment/authorizenet</item> + <item name="methods" xsi:type="array"> + <item name="authorizenet_acceptjs" xsi:type="array"> + <item name="isBillingAddressRequired" xsi:type="boolean">true</item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </argument> + </arguments> + </referenceBlock> + </body> +</page> \ 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 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="payment-method" css="_active: isActive()"> + <div class="payment-method-title field choice"> + <input type="radio" + name="payment[method]" + class="radio" + attr="id: getCode()" + ko-value="getCode()" + ko-checked="isChecked" + click="selectPaymentMethod" + visible="isRadioButtonVisible()" + /> + <label attr="for: getCode()" class="label"> + <span translate="getTitle()"></span> + </label> + </div> + <div class="payment-method-content"> + <each args="getRegion('messages')" render="" /> + <div class="payment-method-billing-address"> + <each args="$parent.getRegion(getBillingAddressFormName())" render="" /> + </div> + <form class="form" id="co-payment-form" method="post" afterRender="initFormElement"> + <render args="ccForm" /> + </form> + <div class="checkout-agreements-block"> + <each args="$parent.getRegion('before-place-order')" render="" /> + </div> + <div class="actions-toolbar"> + <div class="primary"> + <button class="action primary checkout" + type="submit" + click="beforePlaceOrder" + attr="title: $t('Place Order')" + > + <span translate="'Place Order'"></span> + </button> + </div> + </div> + </div> +</div> 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/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/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/Controller/Adminhtml/Dashboard/ProductsViewed.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php index 3907f4a4f71a2..0de1111ffa722 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\HttpGetActionInterface; + +/** + * Get most viewed products controller. + */ +class ProductsViewed extends AjaxBlock implements HttpGetActionInterface { /** * 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/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..a52033b8d2501 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"> 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..99c48b727521a 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. 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..b3a6e5d795cea 100644 --- a/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml +++ b/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml @@ -17,6 +17,7 @@ <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"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickOkConfirmDelete"/> <dontSee selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="dontSeeBackupInGrid"/> 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/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml index be8abbad80cd1..ce1d0a9aecc90 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml @@ -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> 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/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml b/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml index 2376fe9f8006f..244052371e702 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml @@ -60,21 +60,25 @@ <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"/> <waitForElementVisible selector="{{NewOrderSection.successMessage}}" stepKey="waitForSuccessMessage"/> 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/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/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/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/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/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/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/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/CacheInvalidate/Model/PurgeCache.php b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php index 2152f842d4a29..b2aa0d000e9cf 100644 --- a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php +++ b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php @@ -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/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/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/Edit/Action/Attribute/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php index f73c3a11857dd..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 @@ -5,8 +5,6 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab; -use Magento\Customer\Api\Data\GroupInterface; - /** * Products mass update inventory tab * @@ -31,29 +29,20 @@ class Inventory extends \Magento\Backend\Block\Widget implements \Magento\Backen */ protected $disabledFields = []; - /** - * @var \Magento\Framework\Serialize\SerializerInterface - */ - private $serializer; - /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\CatalogInventory\Model\Source\Backorders $backorders * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration * @param array $data - * @param \Magento\Framework\Serialize\SerializerInterface|null $serializer */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\CatalogInventory\Model\Source\Backorders $backorders, \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, - array $data = [], - \Magento\Framework\Serialize\SerializerInterface $serializer = null + array $data = [] ) { $this->_backorders = $backorders; $this->stockConfiguration = $stockConfiguration; - $this->serializer = $serializer ?? \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\SerializerInterface::class); parent::__construct($context, $data); } @@ -85,9 +74,7 @@ public function getFieldSuffix() */ public function getStoreId() { - $storeId = (int)$this->getRequest()->getParam('store'); - - return $storeId; + return (int)$this->getRequest()->getParam('store'); } /** @@ -101,22 +88,6 @@ public function getDefaultConfigValue($field) return $this->stockConfiguration->getDefaultConfigValue($field); } - /** - * Returns min_sale_qty configuration for the ALL Customer Group - * - * @return float - */ - public function getDefaultMinSaleQty() - { - $default = $this->stockConfiguration->getDefaultConfigValue('min_sale_qty'); - if (!is_numeric($default)) { - $default = $this->serializer->unserialize($default); - $default = $default[GroupInterface::CUST_GROUP_ALL] ?? 1; - } - - return (float) $default; - } - /** * Tab settings * 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/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/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/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/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index 50f58efae7127..124ee1abb078e 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -105,7 +105,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); 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/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/Category.php b/app/code/Magento/Catalog/Model/Category.php index 1f62bed93e23f..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,9 +1152,17 @@ public function getIdentities() $identities = [ self::CACHE_TAG . '_' . $this->getId(), ]; + 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(); 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/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/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/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/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/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/Copier.php b/app/code/Magento/Catalog/Model/Product/Copier.php index ce6b4d98bbc9f..53fa11df04b35 100644 --- a/app/code/Magento/Catalog/Model/Product/Copier.php +++ b/app/code/Magento/Catalog/Model/Product/Copier.php @@ -83,7 +83,7 @@ public function copy(Product $product) ? $matches[1] . '-' . ($matches[2] + 1) : $urlKey . '-1'; $duplicate->setUrlKey($urlKey); - $duplicate->setData(Product::URL_PATH, null); + $duplicate->setData('url_path', null); try { $duplicate->save(); $isDuplicateSaved = true; 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..0941aa2478935 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php @@ -28,6 +28,8 @@ public function __construct( } /** + * Perform action on relation/extension attribute + * * @param object $entity * @param array $arguments * @return \Magento\Catalog\Api\Data\ProductInterface|object @@ -35,6 +37,10 @@ public function __construct( */ public function execute($entity, $arguments = []) { + if ($entity->getOptionsSaved()) { + return $entity; + } + $options = $entity->getOptions(); $optionIds = []; diff --git a/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php new file mode 100644 index 0000000000000..f6893a41113e6 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php @@ -0,0 +1,128 @@ +<?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) + */ +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 + */ + 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/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/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/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 618abda0a942d..b5668a12f94a5 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -7,6 +7,7 @@ use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; use Magento\Store\Model\ScopeInterface; /** @@ -83,6 +84,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * + * @param ResourceModelPoolInterface|null $resourceModelPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -97,7 +99,8 @@ public function __construct( \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null, + ResourceModelPoolInterface $resourceModelPool = null ) { parent::__construct( $entityFactory, @@ -110,7 +113,8 @@ public function __construct( $resourceHelper, $universalFactory, $storeManager, - $connection + $connection, + $resourceModelPool ); $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance()->get(ScopeConfigInterface::class); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php index 9ab863cde2704..2e40d13f1ccac 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php @@ -5,8 +5,11 @@ */ namespace Magento\Catalog\Model\ResourceModel\Collection; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** * Catalog EAV collection resource abstract model + * * Implement using different stores for retrieve attribute values * * @api @@ -43,6 +46,7 @@ class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\AbstractCo * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * + * @param ResourceModelPoolInterface|null $resourceModelPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -56,7 +60,8 @@ public function __construct( \Magento\Eav\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ResourceModelPoolInterface $resourceModelPool = null ) { $this->_storeManager = $storeManager; parent::__construct( @@ -69,7 +74,8 @@ public function __construct( $eavEntityFactory, $resourceHelper, $universalFactory, - $connection + $connection, + $resourceModelPool ); } @@ -205,10 +211,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 8aeb52e75c774..136c7e800bf08 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -21,6 +21,7 @@ use Magento\Store\Model\Store; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; /** * Product collection @@ -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,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac * @param TableMaintainer|null $tableMaintainer * @param PriceTableResolver|null $priceTableResolver * @param DimensionFactory|null $dimensionFactory + * @param ResourceModelPoolInterface|null $resourceModelPool + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -349,7 +353,8 @@ public function __construct( MetadataPool $metadataPool = null, TableMaintainer $tableMaintainer = null, PriceTableResolver $priceTableResolver = null, - DimensionFactory $dimensionFactory = null + DimensionFactory $dimensionFactory = null, + ResourceModelPoolInterface $resourceModelPool = null ) { $this->moduleManager = $moduleManager; $this->_catalogProductFlatState = $catalogProductFlatState; @@ -377,7 +382,8 @@ public function __construct( $resourceHelper, $universalFactory, $storeManager, - $connection + $connection, + $resourceModelPool ); $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); $this->priceTableResolver = $priceTableResolver ?: ObjectManager::getInstance()->get(PriceTableResolver::class); @@ -1437,7 +1443,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 = []; 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..a45e2060d7c20 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 @@ -5,6 +5,13 @@ */ namespace Magento\Catalog\Model\ResourceModel\Product\Compare\Item; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** * Catalog Product Compare Items Resource Collection * @@ -75,7 +82,12 @@ 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 - * + * @param ProductLimitationFactory|null $productLimitationFactory + * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory + * @param ResourceModelPoolInterface|null $resourceModelPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -100,7 +112,13 @@ public function __construct( \Magento\Customer\Api\GroupManagementInterface $groupManagement, \Magento\Catalog\Model\ResourceModel\Product\Compare\Item $catalogProductCompareItem, \Magento\Catalog\Helper\Product\Compare $catalogProductCompare, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ProductLimitationFactory $productLimitationFactory = null, + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null, + ResourceModelPoolInterface $resourceModelPool = null ) { $this->_catalogProductCompareItem = $catalogProductCompareItem; $this->_catalogProductCompare = $catalogProductCompare; @@ -124,7 +142,13 @@ public function __construct( $customerSession, $dateTime, $groupManagement, - $connection + $connection, + $productLimitationFactory, + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $resourceModelPool ); } @@ -403,6 +427,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/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/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/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/Pricing/Price/MinimalTierPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php index 387ef9416ef68..a5e573caa381e 100644 --- a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php @@ -29,8 +29,10 @@ public function __construct(CalculatorInterface $calculator) } /** - * Get raw value of "as low as" as a minimal among tier prices - * {@inheritdoc} + * Get raw value of "as low as" as a minimal among tier prices{@inheritdoc} + * + * @param SaleableInterface $saleableItem + * @return float|null */ public function getValue(SaleableInterface $saleableItem) { @@ -49,8 +51,10 @@ public function getValue(SaleableInterface $saleableItem) } /** - * Return calculated amount object that keeps "as low as" value - * {@inheritdoc} + * Return calculated amount object that keeps "as low as" value{@inheritdoc} + * + * @param SaleableInterface $saleableItem + * @return AmountInterface|null */ public function getAmount(SaleableInterface $saleableItem) { @@ -58,6 +62,6 @@ public function getAmount(SaleableInterface $saleableItem) return $value === null ? null - : $this->calculator->getAmount($value, $saleableItem); + : $this->calculator->getAmount($value, $saleableItem, 'tax'); } } diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml index 57f91b78fcbe9..d786795bc3090 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,26 @@ <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> </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..3afdc41888c79 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -71,7 +71,9 @@ <!--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> @@ -160,7 +162,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"/> @@ -235,10 +237,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 +279,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 +337,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 +361,73 @@ </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="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..46329dde278bc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml @@ -30,6 +30,41 @@ <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> + <actionGroup name="changeUseForPromoRuleConditionsProductAttribute"> <arguments> <argument name="option" type="string"/> @@ -47,4 +82,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..2914ecc470220 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml @@ -45,4 +45,50 @@ <fillField selector="{{AdminProductAttributeSetSection.name}}" userInput="{{label}}" stepKey="fillName"/> <click selector="{{AdminProductAttributeSetSection.saveBtn}}" stepKey="clickSave1"/> </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..c9d70319c2877 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"/> @@ -248,4 +261,31 @@ <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> </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/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/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml index 7373d5baea0c5..2d966dde64c4a 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,9 @@ <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> - </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/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/CatalogSpecialPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml new file mode 100644 index 0000000000000..4c6b0749a0f9e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="specialProductPrice" type="catalogSpecialPrice"> + <data key="price">99.99</data> + <data key="store_id">0</data> + <var key="sku" entityType="product2" 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 e832153e45ca6..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> @@ -70,4 +70,44 @@ <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/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index b367cdcab9d8b..134abcaa50354 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -52,6 +52,27 @@ <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="productDropDownAttribute" type="ProductAttribute"> <data key="attribute_code" unique="suffix">attribute</data> <data key="frontend_input">select</data> @@ -73,6 +94,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 +157,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..713c453bb7ad4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml @@ -14,4 +14,10 @@ <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> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index b6442a1af3ff7..0b3a31194ea36 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -35,6 +35,9 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </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> @@ -93,6 +96,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 +135,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 +186,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> @@ -260,7 +309,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"> @@ -480,6 +529,111 @@ <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">Testp</data> <data key="sku" unique="suffix">testsku</data> @@ -519,4 +673,237 @@ <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="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> </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 2be9a7a0358a1..e5070340421a9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml @@ -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..83f0a56c21545 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductLinkWidget" extends="ProductsListWidget"> + <data key="type">Catalog Product Link</data> + <data key="template">Product Link Block Template</data> + </entity> +</entities> 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/Sales/Test/Mftf/Page/StorefrontCreateNewReturnPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml similarity index 63% rename from app/code/Magento/Sales/Test/Mftf/Page/StorefrontCreateNewReturnPage.xml rename to app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml index 2a14f814eac16..e23a503266e33 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontCreateNewReturnPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml @@ -8,7 +8,7 @@ <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 name="AdminNewWidgetPage" url="admin/widget_instance/new/" area="admin" module="Magento_Widget"> + <section name="AdminNewWidgetSelectProductPopupSection"/> </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/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/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml index ef6fb99e88eed..14e714cb2b6b7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml @@ -13,7 +13,7 @@ <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" /> </section> 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..e218f5ae74fc0 --- /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[]')]/..//label" 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 05be20b14acc0..d24c501152b78 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml @@ -23,6 +23,16 @@ <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"/> @@ -36,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"/> @@ -77,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/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..fc78c25ec49fa 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,20 @@ <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"/> </section> </sections> \ No newline at end of file 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..697648cedb7ba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml @@ -20,6 +20,7 @@ <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..e159a4ce5c0b6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.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="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"/> + </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..da282d06145aa 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,11 @@ <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="dropDownAttribute" type="select" selector="//select[@name='product[{{arg}}]']" parameterized="true" timeout="30"/> + <element name="attributeSection" type="div" 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 d4dda07099df3..939974248aabf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml @@ -28,9 +28,11 @@ <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> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml index 30d1088303214..02bdbac313076 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml @@ -8,8 +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="productRowCheckboxBySku" type="block" selector="//div[@id='container']//tr//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]/../td//input[@data-action='select-row']" 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"/> 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/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/StorefrontNavigationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml index e8f35fc6787b7..c6bad0efb3ca7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml @@ -11,5 +11,6 @@ <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"/> </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 5fa2262faba96..8504683648bce 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -13,13 +13,14 @@ <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="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,15 +29,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"/> @@ -50,7 +53,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"/> @@ -67,8 +69,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/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/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/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/AdminChangeProductAttributeSet.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml new file mode 100644 index 0000000000000..3027416ee520b --- /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..09b49011938e8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml @@ -0,0 +1,139 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt 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"/> + </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/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..70edb0ce3ea7d --- /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="verifyFistCustomOptionIsRequired" /> + <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 index 3897769ffbc6a..e4b269dff96ba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml @@ -28,6 +28,7 @@ <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"/> @@ -36,12 +37,14 @@ <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> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml index ff281b3637336..e7ab14c77945a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml @@ -24,14 +24,17 @@ <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"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml index 79d7041bebdb6..6df571f403ac9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml @@ -31,6 +31,7 @@ <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> <actionGroup ref="logout" stepKey="logout"/> </after> + <!--Create a Store--> <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> <waitForPageLoad stepKey="waitForSystemStorePage"/> @@ -40,7 +41,8 @@ <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/>--> + + <!--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"/> @@ -53,25 +55,31 @@ <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 --> + + <!--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"/> 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/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml index ea71f2656b922..d7607b4b269e8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml @@ -28,18 +28,23 @@ <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"/> @@ -50,6 +55,7 @@ <click stepKey="clickOnSaveButton" selector="{{AdminEditProductAttributesSection.Save}}"/> <waitForPageLoad stepKey="waitForUpdatedProductToSave" /> <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 2 record(s) were updated." stepKey="seeAttributeUpateSuccessMsg"/> + <!--Verify product name, sku and updated price--> <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$simpleProduct1.sku$$)}}"/> <waitForPageLoad stepKey="waitForFirstProductToLoad"/> 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..37fbf01a6b9aa 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 --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml index 8dcaa46bf0574..d8d462f850f8f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml @@ -31,20 +31,24 @@ <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"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml index 546655142aaee..479249ca678dd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml @@ -25,9 +25,11 @@ <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"/> @@ -37,11 +39,13 @@ <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"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml index 0155eaa9eb2a7..2cb4a6b6dd436 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml @@ -31,29 +31,35 @@ <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"/> @@ -62,10 +68,12 @@ <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"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml index 16116c22fc4bf..e7c4a8a093e19 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml @@ -31,20 +31,24 @@ <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"/> @@ -53,6 +57,7 @@ <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"/> @@ -65,12 +70,14 @@ <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> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml index 7e84a83c07ee2..3fea9c0eed7ca 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml @@ -26,9 +26,11 @@ <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"/> @@ -45,18 +47,21 @@ <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" /> + <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"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml index 155670230e715..1cb01ac11cb8f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml @@ -27,12 +27,14 @@ <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"/> @@ -42,6 +44,7 @@ <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"/> @@ -57,18 +60,22 @@ <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"/> 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..c9a37ec40e8fa --- /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="verifyFistCustomOptionIsRequired"/> + + <!--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..d67d5b36109e6 --- /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="verifyFistCustomOptionIsRequired" /> + <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/CreateProductAttributeEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest.xml new file mode 100644 index 0000000000000..52022f32fd8ec --- /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_frontend}}" stepKey="seeDefaultIsCorrect"/> + <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option1_frontend}}"/> + <see stepKey="seeOption2Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option2_frontend}}"/> + <see stepKey="seeOption3Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option3_frontend}}"/> + </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_frontend}}" stepKey="seeDefaultIsCorrect"/> + <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttributeWithQuote.attribute_code)}}" userInput="{{dropdownProductAttributeWithQuote.option1_frontend}}"/> + </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_frontend}}" stepKey="seeDefaultIsCorrect"/> + <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option1_frontend}}"/> + <see stepKey="seeOption2Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option2_frontend}}"/> + <see stepKey="seeOption3Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option3_frontend}}"/> + </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 6ecadf1f2ddc2..df4803bcd7906 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml @@ -52,6 +52,10 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Reset Product filter --> + + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + <!-- Delete Store View EN --> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView1"> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/InventoryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/InventoryTest.php index 13e1b33a47d9a..fb8a3d221b029 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/InventoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/InventoryTest.php @@ -5,8 +5,6 @@ */ namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Edit\Action\Attribute\Tab; -use Magento\Customer\Api\Data\GroupInterface; - /** * Class InventoryTest */ @@ -70,8 +68,7 @@ protected function setUp() [ 'context' => $this->contextMock, 'backorders' => $this->backordersMock, - 'stockConfiguration' => $this->stockConfigurationMock, - 'serializer' => new \Magento\Framework\Serialize\Serializer\Json(), + 'stockConfiguration' => $this->stockConfigurationMock ] ); } @@ -129,32 +126,6 @@ public function testGetDefaultConfigValue() $this->assertEquals('return-value', $this->inventory->getDefaultConfigValue('field-name')); } - /** - * @dataProvider getDefaultMinSaleQtyDataProvider - * @param string $expected - * @param string $default - */ - public function testGetDefaultMinSaleQty($expected, $default) - { - $this->stockConfigurationMock->method('getDefaultConfigValue')->willReturn($default); - $this->assertEquals($expected, $this->inventory->getDefaultMinSaleQty()); - } - - public function getDefaultMinSaleQtyDataProvider() - { - return [ - 'single-default-value' => [ - 22, '22' - ], - 'no-default-for-all-group' => [ - 1, json_encode(['12' => '111']) - ], - 'default-for-all-group' => [ - 5, json_encode(['12' => '111', GroupInterface::CUST_GROUP_ALL => '5']) - ] - ]; - } - /** * Run test getTabLabel method * 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/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/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/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php index 034b04b6a757d..cfb54c3aefd0f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php @@ -29,12 +29,12 @@ ], ], 'renderer_attribute_with_invalid_value' => [ - '<?xml version="1.0"?><config><option name="name_one" renderer="true12"><inputType name="name_one"/>' . + '<?xml version="1.0"?><config><option name="name_one" renderer="123true"><inputType name="name_one"/>' . '</option></config>', [ - "Element 'option', attribute 'renderer': [facet 'pattern'] The value 'true12' is not accepted by the " . - "pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", - "Element 'option', attribute 'renderer': 'true12' is not a valid value of the atomic" . + "Element 'option', attribute 'renderer': [facet 'pattern'] The value '123true' is not accepted by the " . + "pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", + "Element 'option', attribute 'renderer': '123true' is not a valid value of the atomic" . " type 'modelName'.\nLine: 1\n" ], ], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php index e1847bea53fcb..868252da8190c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php @@ -23,7 +23,7 @@ '<?xml version="1.0"?><config><type name="some_name" modelInstance="123" /></config>', [ "Element 'type', attribute 'modelInstance': [facet 'pattern'] The value '123' is not accepted by the" . - " pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + " pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'type', attribute 'modelInstance': '123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -57,7 +57,7 @@ '<?xml version="1.0"?><config><type name="some_name"><priceModel instance="123123" /></type></config>', [ "Element 'priceModel', attribute 'instance': [facet 'pattern'] The value '123123' is not accepted " . - "by the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'priceModel', attribute 'instance': '123123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -66,7 +66,7 @@ '<?xml version="1.0"?><config><type name="some_name"><indexerModel instance="123" /></type></config>', [ "Element 'indexerModel', attribute 'instance': [facet 'pattern'] The value '123' is not accepted by " . - "the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'indexerModel', attribute 'instance': '123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -83,7 +83,7 @@ '<?xml version="1.0"?><config><type name="some_name"><stockIndexerModel instance="1234"/></type></config>', [ "Element 'stockIndexerModel', attribute 'instance': [facet 'pattern'] The value '1234' is not " . - "accepted by the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'stockIndexerModel', attribute 'instance': '1234' is not a valid value of the atomic " . "type 'modelName'.\nLine: 1\n" ], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml index 7edbc399a9476..701338774baa5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml @@ -15,6 +15,14 @@ <stockIndexerModel instance="instance_name"/> </type> <type label="some_label" name="some_name2" modelInstance="model_name"> + <allowedSelectionTypes> + <type name="some_name" /> + </allowedSelectionTypes> + <priceModel instance="instance_name_with_digits_123" /> + <indexerModel instance="instance_name_with_digits_123" /> + <stockIndexerModel instance="instance_name_with_digits_123"/> + </type> + <type label="some_label" name="some_name3" modelInstance="model_name"> <allowedSelectionTypes> <type name="some_name" /> </allowedSelectionTypes> @@ -25,5 +33,6 @@ <composableTypes> <type name="some_name"/> <type name="some_name2"/> + <type name="some_name3"/> </composableTypes> </config> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php index 3eb219ee2932b..5da5625189ee3 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 @@ -5,13 +5,33 @@ */ namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product; +use Magento\Catalog\Model\Indexer; +use Magento\Catalog\Model\Product as ProductModel; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; use Magento\Framework\DB\Select; +use Magento\Eav\Model\Entity\AbstractEntity; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\EntityFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection; +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; +use Magento\Framework\DB; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CollectionTest extends \PHPUnit\Framework\TestCase +class CollectionTest extends TestCase { /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager @@ -24,12 +44,12 @@ class CollectionTest extends \PHPUnit\Framework\TestCase protected $selectMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject|DB\Adapter\AdapterInterface */ protected $connectionMock; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Collection + * @var ProductResource\Collection */ protected $collection; @@ -70,128 +90,50 @@ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->entityFactory = $this->createMock(\Magento\Framework\Data\Collection\EntityFactory::class); - $logger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $fetchStrategy = $this->getMockBuilder(\Magento\Framework\Data\Collection\Db\FetchStrategyInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $eventManager = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) - ->disableOriginalConstructor() - ->getMock(); - $resource = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) - ->disableOriginalConstructor() - ->getMock(); - $eavEntityFactory = $this->getMockBuilder(\Magento\Eav\Model\EntityFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $resourceHelper = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Helper::class) - ->disableOriginalConstructor() - ->getMock(); - $universalFactory = $this->getMockBuilder(\Magento\Framework\Validator\UniversalFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getStore', 'getId', 'getWebsiteId']) - ->getMockForAbstractClass(); - $moduleManager = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) - ->disableOriginalConstructor() - ->getMock(); - $catalogProductFlatState = $this->getMockBuilder(\Magento\Catalog\Model\Indexer\Product\Flat\State::class) - ->disableOriginalConstructor() - ->getMock(); - $scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $productOptionFactory = $this->getMockBuilder(\Magento\Catalog\Model\Product\OptionFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $catalogUrl = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Url::class) - ->disableOriginalConstructor() - ->getMock(); - $localeDate = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->getMock(); - $dateTime = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime::class) - ->disableOriginalConstructor() - ->getMock(); - $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( - $this->entityMock - ); + $this->selectMock = $this->createMock(DB\Select::class); + $this->connectionMock = $this->createMock(DB\Adapter\AdapterInterface::class); + $this->connectionMock->expects($this->atLeastOnce())->method('select')->willReturn($this->selectMock); + $this->entityMock = $this->createMock(AbstractEntity::class); $this->entityMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); $this->entityMock->expects($this->once())->method('getDefaultAttributes')->willReturn([]); - $this->entityMock->expects($this->any())->method('getTable')->willReturnArgument(0); - $this->connectionMock->expects($this->atLeastOnce())->method('select')->willReturn($this->selectMock); + $this->entityMock->method('getTable')->willReturnArgument(0); + $this->galleryResourceMock = $this->createMock(ProductResource\Gallery::class); + $this->metadataPoolMock = $this->createMock(MetadataPool::class); + $this->galleryReadHandlerMock = $this->createMock(ProductModel\Gallery\ReadHandler::class); - $productLimitationMock = $this->createMock( - \Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation::class - ); - $productLimitationFactoryMock = $this->getMockBuilder( - ProductLimitationFactory::class - )->disableOriginalConstructor()->setMethods(['create'])->getMock(); + $storeStub = $this->createMock(StoreInterface::class); + $storeStub->method('getId')->willReturn(1); + $storeStub->method('getWebsiteId')->willReturn(1); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->storeManager->method('getStore')->willReturn($storeStub); + $resourceModelPool = $this->createMock(ResourceModelPoolInterface::class); + $resourceModelPool->expects($this->exactly(1))->method('get')->willReturn($this->entityMock); + $productLimitationFactoryMock = $this->createPartialMock(ProductLimitationFactory::class, ['create']); $productLimitationFactoryMock->method('create') - ->willReturn($productLimitationMock); + ->willReturn($this->createMock(ProductResource\Collection\ProductLimitation::class)); $this->collection = $this->objectManager->getObject( - \Magento\Catalog\Model\ResourceModel\Product\Collection::class, + ProductResource\Collection::class, [ 'entityFactory' => $this->entityFactory, - 'logger' => $logger, - 'fetchStrategy' => $fetchStrategy, - 'eventManager' => $eventManager, - 'eavConfig' => $eavConfig, - 'resource' => $resource, - 'eavEntityFactory' => $eavEntityFactory, - 'resourceHelper' => $resourceHelper, - 'universalFactory' => $universalFactory, + 'logger' => $this->createMock(LoggerInterface::class), + 'fetchStrategy' => $this->createMock(FetchStrategyInterface::class), + 'eventManager' => $this->createMock(Event\ManagerInterface::class), + 'eavConfig' => $this->createMock(\Magento\Eav\Model\Config::class), + 'resource' => $this->createMock(ResourceConnection::class), + 'eavEntityFactory' => $this->createMock(EntityFactory::class), + 'resourceHelper' => $this->createMock(\Magento\Catalog\Model\ResourceModel\Helper::class), + 'resourceModelPool' => $resourceModelPool, 'storeManager' => $this->storeManager, - 'moduleManager' => $moduleManager, - 'catalogProductFlatState' => $catalogProductFlatState, - 'scopeConfig' => $scopeConfig, - 'productOptionFactory' => $productOptionFactory, - 'catalogUrl' => $catalogUrl, - 'localeDate' => $localeDate, - 'customerSession' => $customerSession, - 'dateTime' => $dateTime, - 'groupManagement' => $groupManagement, + 'moduleManager' => $this->createMock(\Magento\Framework\Module\Manager::class), + 'catalogProductFlatState' => $this->createMock(Indexer\Product\Flat\State::class), + 'scopeConfig' => $this->createMock(ScopeConfigInterface::class), + 'productOptionFactory' => $this->createMock(ProductModel\OptionFactory::class), + 'catalogUrl' => $this->createMock(\Magento\Catalog\Model\ResourceModel\Url::class), + 'localeDate' => $this->createMock(TimezoneInterface::class), + 'customerSession' => $this->createMock(\Magento\Customer\Model\Session::class), + 'dateTime' => $this->createMock(\Magento\Framework\Stdlib\DateTime::class), + 'groupManagement' => $this->createMock(\Magento\Customer\Api\GroupManagementInterface::class), 'connection' => $this->connectionMock, 'productLimitationFactory' => $productLimitationFactoryMock, 'metadataPool' => $this->metadataPoolMock, @@ -216,9 +158,8 @@ public function testAddProductCategoriesFilter() $condition = ['in' => [1, 2]]; $values = [1, 2]; $conditionType = 'nin'; - $preparedSql = "category_id IN(1,2)"; - $tableName = "catalog_category_product"; - $this->connectionMock->expects($this->any())->method('getId')->willReturn(1); + $preparedSql = 'category_id IN(1,2)'; + $tableName = 'catalog_category_product'; $this->connectionMock->expects($this->exactly(2))->method('prepareSqlCondition')->withConsecutive( ['cat.category_id', $condition], ['e.entity_id', [$conditionType => $this->selectMock]] @@ -243,19 +184,14 @@ public function testAddMediaGalleryData() $rowId = 4; $linkField = 'row_id'; $mediaGalleriesMock = [[$linkField => $rowId]]; - $itemMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + /** @var ProductModel|\PHPUnit_Framework_MockObject_MockObject $itemMock */ + $itemMock = $this->getMockBuilder(ProductModel::class) ->disableOriginalConstructor() ->setMethods(['getOrigData']) ->getMock(); - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() - ->getMock(); - $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->getMock(); - $metadataMock = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $attributeMock = $this->createMock(AbstractAttribute::class); + $selectMock = $this->createMock(DB\Select::class); + $metadataMock = $this->createMock(EntityMetadataInterface::class); $this->collection->addItem($itemMock); $this->galleryResourceMock->expects($this->once())->method('createBatchBaseSelect')->willReturn($selectMock); $attributeMock->expects($this->once())->method('getAttributeId')->willReturn($attributeId); @@ -285,25 +221,15 @@ public function testAddMediaGalleryData() public function testAddTierPriceDataByGroupId() { $customerGroupId = 2; - $itemMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->setMethods(['getData']) - ->getMock(); - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) + /** @var ProductModel|\PHPUnit_Framework_MockObject_MockObject $itemMock */ + $itemMock = $this->createMock(ProductModel::class); + $attributeMock = $this->getMockBuilder(AbstractAttribute::class) ->disableOriginalConstructor() ->setMethods(['isScopeGlobal', 'getBackend']) ->getMock(); - $backend = $this->getMockBuilder(\Magento\Catalog\Model\Product\Attribute\Backend\Tierprice::class) - ->disableOriginalConstructor() - ->getMock(); - $resource = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\GroupPrice\AbstractGroupPrice::class - ) - ->disableOriginalConstructor() - ->getMock(); - $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->getMock(); + $backend = $this->createMock(ProductModel\Attribute\Backend\Tierprice::class); + $resource = $this->createMock(ProductResource\Attribute\Backend\GroupPrice\AbstractGroupPrice::class); + $select = $this->createMock(DB\Select::class); $this->connectionMock->expects($this->once())->method('getAutoIncrementField')->willReturn('entity_id'); $this->collection->addItem($itemMock); $itemMock->expects($this->atLeastOnce())->method('getData')->with('entity_id')->willReturn(1); @@ -313,7 +239,6 @@ public function testAddTierPriceDataByGroupId() ->willReturn($attributeMock); $attributeMock->expects($this->atLeastOnce())->method('getBackend')->willReturn($backend); $attributeMock->expects($this->once())->method('isScopeGlobal')->willReturn(false); - $this->storeManager->expects($this->once())->method('getWebsiteId')->willReturn(1); $backend->expects($this->once())->method('getResource')->willReturn($resource); $resource->expects($this->once())->method('getSelect')->willReturn($select); $select->expects($this->once())->method('columns')->with(['product_id' => 'entity_id'])->willReturnSelf(); @@ -340,25 +265,22 @@ public function testAddTierPriceDataByGroupId() */ public function testAddTierPriceData() { - $itemMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + /** @var ProductModel|\PHPUnit_Framework_MockObject_MockObject $itemMock */ + $itemMock = $this->getMockBuilder(ProductModel::class) ->disableOriginalConstructor() ->setMethods(['getData']) ->getMock(); - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) + $attributeMock = $this->getMockBuilder(AbstractAttribute::class) ->disableOriginalConstructor() ->setMethods(['isScopeGlobal', 'getBackend']) ->getMock(); - $backend = $this->getMockBuilder(\Magento\Catalog\Model\Product\Attribute\Backend\Tierprice::class) - ->disableOriginalConstructor() - ->getMock(); + $backend = $this->createMock(ProductModel\Attribute\Backend\Tierprice::class); $resource = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\GroupPrice\AbstractGroupPrice::class + ProductResource\Attribute\Backend\GroupPrice\AbstractGroupPrice::class ) ->disableOriginalConstructor() ->getMock(); - $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->getMock(); + $select = $this->createMock(DB\Select::class); $this->connectionMock->expects($this->once())->method('getAutoIncrementField')->willReturn('entity_id'); $this->collection->addItem($itemMock); $itemMock->expects($this->atLeastOnce())->method('getData')->with('entity_id')->willReturn(1); @@ -368,7 +290,6 @@ public function testAddTierPriceData() ->willReturn($attributeMock); $attributeMock->expects($this->atLeastOnce())->method('getBackend')->willReturn($backend); $attributeMock->expects($this->once())->method('isScopeGlobal')->willReturn(false); - $this->storeManager->expects($this->once())->method('getWebsiteId')->willReturn(1); $backend->expects($this->once())->method('getResource')->willReturn($resource); $resource->expects($this->once())->method('getSelect')->willReturn($select); $select->expects($this->once())->method('columns')->with(['product_id' => 'entity_id'])->willReturnSelf(); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Link/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Link/Product/CollectionTest.php index 596148b627506..80180d2033ce5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Link/Product/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Link/Product/CollectionTest.php @@ -7,6 +7,8 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -26,7 +28,7 @@ class CollectionTest extends \PHPUnit\Framework\TestCase /** @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $loggerMock; - /** @var \Magento\Framework\Data\Collection\Db\FetchStrategyInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var FetchStrategyInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $fetchStrategyMock; /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -44,8 +46,8 @@ class CollectionTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Catalog\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ protected $helperMock; - /** @var \Magento\Framework\Validator\UniversalFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $universalFactoryMock; + /** @var ResourceModelPoolInterface|\PHPUnit_Framework_MockObject_MockObject */ + protected $resourceModelPoolMock; /** @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $storeManagerMock; @@ -79,29 +81,23 @@ protected function setUp() $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->entityFactoryMock = $this->createMock(\Magento\Framework\Data\Collection\EntityFactory::class); $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->fetchStrategyMock = $this->createMock( - \Magento\Framework\Data\Collection\Db\FetchStrategyInterface::class - ); + $this->fetchStrategyMock = $this->createMock(FetchStrategyInterface::class); $this->managerInterfaceMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); $this->configMock = $this->createMock(\Magento\Eav\Model\Config::class); $this->resourceMock = $this->createMock(\Magento\Framework\App\ResourceConnection::class); $this->entityFactoryMock2 = $this->createMock(\Magento\Eav\Model\EntityFactory::class); $this->helperMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Helper::class); $entity = $this->createMock(\Magento\Eav\Model\Entity\AbstractEntity::class); - $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->getMock(); - $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\Pdo\Mysql::class) - ->disableOriginalConstructor() - ->getMock(); + $select = $this->createMock(\Magento\Framework\DB\Select::class); + $connection = $this->createMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class); $connection->expects($this->any()) ->method('select') ->willReturn($select); $entity->expects($this->any())->method('getConnection')->will($this->returnValue($connection)); $entity->expects($this->any())->method('getDefaultAttributes')->will($this->returnValue([])); - $this->universalFactoryMock = $this->createMock(\Magento\Framework\Validator\UniversalFactory::class); - $this->universalFactoryMock->expects($this->any())->method('create')->will($this->returnValue($entity)); - $this->storeManagerMock = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class); + $this->resourceModelPoolMock = $this->createMock(ResourceModelPoolInterface::class); + $this->resourceModelPoolMock->expects($this->any())->method('get')->will($this->returnValue($entity)); + $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->storeManagerMock ->expects($this->any()) ->method('getStore') @@ -118,9 +114,7 @@ function ($store) { $this->timezoneInterfaceMock = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); $this->sessionMock = $this->createMock(\Magento\Customer\Model\Session::class); $this->dateTimeMock = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); - $productLimitationFactoryMock = $this->getMockBuilder( - ProductLimitationFactory::class - )->disableOriginalConstructor()->setMethods(['create'])->getMock(); + $productLimitationFactoryMock = $this->createPartialMock(ProductLimitationFactory::class, ['create']); $productLimitationFactoryMock->method('create') ->willReturn($this->createMock(ProductLimitation::class)); @@ -136,7 +130,7 @@ function ($store) { 'resource' => $this->resourceMock, 'eavEntityFactory' => $this->entityFactoryMock2, 'resourceHelper' => $this->helperMock, - 'universalFactory' => $this->universalFactoryMock, + 'resourceModelPool' => $this->resourceModelPoolMock, 'storeManager' => $this->storeManagerMock, 'catalogData' => $this->catalogHelperMock, 'catalogProductFlatState' => $this->stateMock, 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/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/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/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php index 2a4d2ff52d479..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 @@ -389,6 +389,9 @@ private function addAdvancedPriceLink() '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/CustomOptions.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php index 86f1db2022cc9..f8f82511cc12f 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,10 +868,9 @@ 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, 'sortOrder' => $sortOrder, ], ], 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/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/etc/frontend/di.xml b/app/code/Magento/Catalog/etc/frontend/di.xml index ee9c5b29da894..793a2291f599c 100644 --- a/app/code/Magento/Catalog/etc/frontend/di.xml +++ b/app/code/Magento/Catalog/etc/frontend/di.xml @@ -120,4 +120,5 @@ <plugin name="catalog_app_action_dispatch_controller_context_plugin" type="Magento\Catalog\Plugin\Framework\App\Action\ContextPlugin" /> </type> + <preference for="Magento\Catalog\Model\Product\Type\Price" type="Magento\Catalog\Model\Product\Type\FrontSpecialPrice" /> </config> diff --git a/app/code/Magento/Catalog/etc/product_options.xsd b/app/code/Magento/Catalog/etc/product_options.xsd index 3bc24a9099262..734c8f378d5d7 100644 --- a/app/code/Magento/Catalog/etc/product_options.xsd +++ b/app/code/Magento/Catalog/etc/product_options.xsd @@ -61,11 +61,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [a-zA-Z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/Catalog/etc/product_types_base.xsd b/app/code/Magento/Catalog/etc/product_types_base.xsd index 6cc35fd7bee37..dec952bcf492e 100644 --- a/app/code/Magento/Catalog/etc/product_types_base.xsd +++ b/app/code/Magento/Catalog/etc/product_types_base.xsd @@ -92,11 +92,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [a-zA-Z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/Catalog/etc/webapi_rest/di.xml b/app/code/Magento/Catalog/etc/webapi_rest/di.xml index 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 96e07ceb4d305..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->getDefaultMinSaleQty() * 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..578281f44c4cf 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 @@ -190,6 +190,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/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/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/details.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml index af664051b1431..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 @@ -22,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/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/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/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 e910a5c8be4cd..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\Framework\Exception\InputException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Filter; use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search; @@ -17,7 +16,6 @@ use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\SearchFilter; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Catalog\Model\Layer\Resolver; -use Magento\Framework\Api\Search\SearchCriteriaInterface; /** * Products field resolver, used for GraphQL request processing. @@ -82,10 +80,10 @@ public function resolve( } elseif (isset($args['search'])) { $layerType = Resolver::CATALOG_LAYER_SEARCH; $this->searchFilter->add($args['search'], $searchCriteria); - $searchResult = $this->getSearchResult($this->searchQuery, $searchCriteria, $info); + $searchResult = $this->searchQuery->getResult($searchCriteria, $info); } else { $layerType = Resolver::CATALOG_LAYER_CATEGORY; - $searchResult = $this->getSearchResult($this->filterQuery, $searchCriteria, $info); + $searchResult = $this->filterQuery->getResult($searchCriteria, $info); } //possible division by 0 if ($searchCriteria->getPageSize()) { @@ -117,25 +115,4 @@ public function resolve( return $data; } - - /** - * Get search result. - * - * @param Filter|Search $query - * @param SearchCriteriaInterface $searchCriteria - * @param ResolveInfo $info - * - * @return \Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult - * @throws GraphQlInputException - */ - private function getSearchResult($query, SearchCriteriaInterface $searchCriteria, ResolveInfo $info) - { - try { - $searchResult = $query->getResult($searchCriteria, $info); - } catch (InputException $e) { - throw new GraphQlInputException(__($e->getMessage())); - } - - return $searchResult; - } } 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/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index 7e18ac34f0fcc..b5622e948b7cd 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> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 45f3a4c83be7b..778d0783305b7 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") { } diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 52ab8d49ec546..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; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index c525b69b91e7f..404c31296e4dd 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -133,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. */ @@ -309,7 +299,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity 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', + 'invalidNewToDateValue' => 'Make sure new_to_date is later than or the same as new_from_date', ]; //@codingStandardsIgnoreEnd @@ -331,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', @@ -2556,7 +2546,7 @@ public function validateRow(array $rowData, $rowNum) $rowNum, $rowData[self::COL_NAME], $message, - ProcessingError::ERROR_LEVEL_NOT_CRITICAL + $errorLevel ) ->getErrorAggregator() ->addRowToSkip($rowNum); @@ -2564,16 +2554,16 @@ public function validateRow(array $rowData, $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->skipRow( $rowNum, - ValidatorInterface::ERROR_NEW_TO_DATE, + 'invalidNewToDateValue', $errorLevel, - $rowData[self::COL_NEW_TO_DATE] + $rowData['new_to_date'] ); } } 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/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/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/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/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/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/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/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/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/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/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/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/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index b4b15554f6029..e3f61af771f8c 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -3,24 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogSearch\Model\ResourceModel\Advanced; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; 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\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; 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 +75,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 +139,15 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param SearchResultFactory|null $searchResultFactory * @param ProductLimitationFactory|null $productLimitationFactory * @param MetadataPool|null $metadataPool - * + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory + * @param ResourceModelPoolInterface|null $resourceModelPool + * @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 +176,33 @@ public function __construct( \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, SearchResultFactory $searchResultFactory = null, ProductLimitationFactory $productLimitationFactory = null, - MetadataPool $metadataPool = null + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null, + ResourceModelPoolInterface $resourceModelPool = 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, @@ -148,7 +225,11 @@ public function __construct( $groupManagement, $connection, $productLimitationFactory, - $metadataPool + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $resourceModelPool ); } @@ -167,11 +248,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 +338,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 +492,7 @@ private function getSearchCriteriaBuilder() } /** - * Get fielter builder. + * Get filter builder. * * @return FilterBuilder */ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index e6cfe8ca112f7..2c36b150fed07 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -3,13 +3,26 @@ * 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\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +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\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; use Magento\Framework\Search\Response\QueryResponse; use Magento\Framework\Search\Request\EmptyRequestDataException; use Magento\Framework\Search\Request\NonExistingRequestNameException; @@ -17,6 +30,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 +40,7 @@ * @api * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -60,11 +75,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $queryText; - /** - * @var string|null - */ - private $relevanceOrderDirection = null; - /** * @var string */ @@ -101,6 +111,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 +167,19 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param SearchResultFactory|null $searchResultFactory * @param ProductLimitationFactory|null $productLimitationFactory * @param MetadataPool|null $metadataPool - * + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory + * @param ResourceModelPoolInterface|null $resourceModelPool + * @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 +209,18 @@ public function __construct( $searchRequestName = 'catalog_view_container', SearchResultFactory $searchResultFactory = null, ProductLimitationFactory $productLimitationFactory = null, - MetadataPool $metadataPool = null + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null, + ResourceModelPoolInterface $resourceModelPool = 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) { @@ -192,12 +249,29 @@ public function __construct( $groupManagement, $connection, $productLimitationFactory, - $metadataPool + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $resourceModelPool ); $this->requestBuilder = $requestBuilder; $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 +405,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 +439,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 +539,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 +585,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 +607,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..fd948616c005b 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php @@ -6,6 +6,13 @@ namespace Magento\CatalogSearch\Model\ResourceModel\Search; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** * Search collection * @@ -60,7 +67,12 @@ 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 - * + * @param ProductLimitationFactory|null $productLimitationFactory + * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory + * @param ResourceModelPoolInterface|null $resourceModelPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -84,7 +96,13 @@ public function __construct( \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Customer\Api\GroupManagementInterface $groupManagement, \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeCollectionFactory, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ProductLimitationFactory $productLimitationFactory = null, + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null, + ResourceModelPoolInterface $resourceModelPool = null ) { $this->_attributeCollectionFactory = $attributeCollectionFactory; parent::__construct( @@ -107,7 +125,13 @@ public function __construct( $customerSession, $dateTime, $groupManagement, - $connection + $connection, + $productLimitationFactory, + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $resourceModelPool ); } 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/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/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml index 387a7547f4daf..6b913e5b458e6 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml @@ -48,7 +48,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 --> 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/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/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..fe29fa1ece011 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 @@ -61,7 +67,7 @@ protected function setUp() $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->eavConfig = $this->createMock(\Magento\Eav\Model\Config::class); $storeManager = $this->getStoreManager(); - $universalFactory = $this->getUniversalFactory(); + $resourceModelPool = $this->getResourceModelPool(); $this->criteriaBuilder = $this->getCriteriaBuilder(); $this->filterBuilder = $this->createMock(\Magento\Framework\Api\FilterBuilder::class); $this->temporaryStorageFactory = $this->createMock( @@ -79,17 +85,57 @@ 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, [ 'eavConfig' => $this->eavConfig, 'storeManager' => $storeManager, - 'universalFactory' => $universalFactory, + 'resourceModelPool' => $resourceModelPool, 'searchCriteriaBuilder' => $this->criteriaBuilder, 'filterBuilder' => $this->filterBuilder, '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/BaseCollection.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/BaseCollection.php index 9ea103e23d2a7..5a5106593af8b 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/BaseCollection.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/BaseCollection.php @@ -5,6 +5,8 @@ */ namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** * Base class for Collection tests. * @@ -42,19 +44,17 @@ protected function getStoreManager() } /** - * Get mock for UniversalFactory so Collection can be used. + * Get mock for ResourceModelPool so Collection can be used. * - * @return \PHPUnit_Framework_MockObject_MockObject + * @return \PHPUnit_Framework_MockObject_MockObject|ResourceModelPoolInterface */ - protected function getUniversalFactory() + protected function getResourceModelPool() { $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\Pdo\Mysql::class) ->disableOriginalConstructor() ->setMethods(['select']) ->getMockForAbstractClass(); - $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->getMock(); + $select = $this->createMock(\Magento\Framework\DB\Select::class); $connection->expects($this->any())->method('select')->willReturn($select); $entity = $this->getMockBuilder(\Magento\Eav\Model\Entity\AbstractEntity::class) @@ -74,14 +74,14 @@ protected function getUniversalFactory() ->method('getEntityTable') ->willReturn('table'); - $universalFactory = $this->getMockBuilder(\Magento\Framework\Validator\UniversalFactory::class) - ->setMethods(['create']) + $resourceModelPool = $this->getMockBuilder(ResourceModelPoolInterface::class) + ->setMethods(['get']) ->disableOriginalConstructor() ->getMock(); - $universalFactory->expects($this->once()) - ->method('create') + $resourceModelPool->expects($this->once()) + ->method('get') ->willReturn($entity); - return $universalFactory; + return $resourceModelPool; } } 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..f1c2161a052e0 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; @@ -43,7 +49,7 @@ class CollectionTest extends BaseCollection /** * @var MockObject */ - private $universalFactory; + private $resourceModelPool; /** * @var MockObject @@ -72,7 +78,7 @@ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->storeManager = $this->getStoreManager(); - $this->universalFactory = $this->getUniversalFactory(); + $this->resourceModelPool = $this->getResourceModelPool(); $this->scopeConfig = $this->getScopeConfig(); $this->criteriaBuilder = $this->getCriteriaBuilder(); $this->filterBuilder = $this->getFilterBuilder(); @@ -97,15 +103,53 @@ 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, [ 'storeManager' => $this->storeManager, - 'universalFactory' => $this->universalFactory, + 'resourceModelPool' => $this->resourceModelPool, '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/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/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/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index 9aaa384776855..7b60c85049767 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -263,6 +263,7 @@ 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; 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/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml new file mode 100644 index 0000000000000..593df1c5bc6e1 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.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="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="A total of 1 record(s) were updated." stepKey="seeSaveSuccess"/> + + <!--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/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 fad3ed621cff9..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'; @@ -22,12 +23,12 @@ $showCart = true; $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 . '-->' ?> @@ -51,26 +52,28 @@ <?php if ($templateType): ?> <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> <?php endif; ?> - <?php echo $block->getProductPriceHtml($_item, $type); ?> + + <?= $block->getProductPriceHtml($_item, $type) ?> + + <?= $block->getProductDetailsHtml($_item) ?> + <?php if ($showWishlist || $showCompare || $showCart): ?> <div class="product-item-inner"> <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> + <?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> - <?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 endif; ?> + </form> <?php else: ?> <?php if ($_item->getIsSalable()): ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></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/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/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/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml index 2c5444da6d974..b67b7451d5968 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> 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/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/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/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/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 0d3c6e419cc07..dcfb12fd4e965 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -26,6 +26,7 @@ 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"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml index ad2a43eb90c8c..6838824400b96 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml @@ -19,5 +19,9 @@ <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/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index a182a3357a9ce..d825e10395145 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -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/StorefrontMiniCartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml index a894f2fbb1af9..bdb02835c6276 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"/> 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/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/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/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/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/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/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/noItems.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml index 1c0c221a550cd..67ac4a9335565 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml @@ -13,3 +13,10 @@ $block->escapeUrl($block->getContinueShoppingUrl())) ?></p> <?= $block->getChildHtml('shopping.cart.table.after') ?> </div> +<script type="text/x-magento-init"> +{ + "*": { + "Magento_Checkout/js/empty-cart": {} + } +} +</script> \ No newline at end of file diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js index ce1527b3d72d6..1920bc4d7ac41 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js @@ -14,7 +14,8 @@ define([ $.widget('mage.updateShoppingCart', { options: { validationURL: '', - eventName: 'updateCartItemQty' + eventName: 'updateCartItemQty', + updateCartActionContainer: '' }, /** @inheritdoc */ @@ -31,7 +32,9 @@ define([ * @return {Boolean} */ onSubmit: function (event) { - if (!this.options.validationURL) { + var action = this.element.find(this.options.updateCartActionContainer).val(); + + if (!this.options.validationURL || action === 'empty_cart') { return true; } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js new file mode 100644 index 0000000000000..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/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/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/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/view/billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js index 6f9a1a46826da..d68b0682eb511 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js @@ -202,6 +202,13 @@ function ( } }, + /** + * Manage cancel button visibility + */ + canUseCancelBillingAddress: ko.computed(function () { + return quote.billingAddress() || lastSelectedBillingAddress; + }), + /** * Restore billing address */ diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index a2f8c8c56ff33..5e29fa209a641 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -81,6 +81,7 @@ define([ maxItemsToDisplay: window.checkout.maxItemsToDisplay, cart: {}, + // jscs:disable requireCamelCaseOrUpperCaseIdentifiers /** * @override */ @@ -101,12 +102,16 @@ define([ self.isLoading(true); }); - if (cartData()['website_id'] !== window.checkout.websiteId) { + if (cartData().website_id !== window.checkout.websiteId || + cartData().store_id !== window.checkout.storeId + ) { customerData.reload(['cart'], false); } return this._super(); }, + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + isLoading: ko.observable(false), initSidebar: initSidebar, diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html index 5f735fbb4daa9..63edb5057b933 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html @@ -22,7 +22,7 @@ <button class="action action-update" type="button" data-bind="click: updateAddress"> <span data-bind="i18n: 'Update'"></span> </button> - <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit"> + <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit, visible: canUseCancelBillingAddress()"> <span data-bind="i18n: 'Cancel'"></span> </button> </div> diff --git a/app/code/Magento/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/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/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/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/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/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 e2f22d29d3456..07e43347d9ddd 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml @@ -44,7 +44,16 @@ <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"/> @@ -60,4 +69,50 @@ <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/CmsPageData.xml b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml index d8a8401c31f15..2ec2eccba2344 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml @@ -86,4 +86,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/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/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/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/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml index 03edc69e6d625..05b7dfeeb3953 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml @@ -16,6 +16,7 @@ <description value="Admin should be able to add image to WYSIWYG content of Block"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84376"/> + <group value="WYSIWYGDisabled" /> </annotations> <before> <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> 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..b4bcdaadf9a09 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.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="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"/> + <group value="WYSIWYGDisabled"/> + </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 index 65fabfe25e817..2c351a12af72e 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml @@ -11,13 +11,12 @@ <test name="StoreViewLanguageCorrectSwitchTest"> <annotations> <features value="Cms"/> - <stories value="Store View (language) switch leads to 404"/> - <group 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"/> @@ -37,7 +36,7 @@ <!-- Create StoreView --> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> - <argument name="customStore" value="NewStoreViewData"/> + <argument name="customStore" value="NewStoreViewData"/> </actionGroup> <!-- Add StoreView To Cms Page--> @@ -51,10 +50,18 @@ <see userInput="$$createFirstCmsPage.title$$" stepKey="seePageTitle"/> <!-- Switch StoreView and check that Cms Page is open --> - <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher"/> - <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropDown"/> - <click selector="{{StorefrontHeaderSection.storeViewOption(NewStoreViewData.code)}}" stepKey="selectStoreView"/> + <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/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/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php index c237d0ea9963a..2c4b8a8dc48d2 100644 --- a/app/code/Magento/Config/App/Config/Type/System.php +++ b/app/code/Magento/Config/App/Config/Type/System.php @@ -13,6 +13,7 @@ use Magento\Config\App\Config\Type\System\Reader; use Magento\Framework\App\ScopeInterface; use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Lock\LockManagerInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\Model\Config\Processor\Fallback; use Magento\Store\Model\ScopeInterface as StoreScope; @@ -27,9 +28,37 @@ */ 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'; + /** + * Timeout between retrieves to load the configuration from the cache. + * + * Value of the variable in microseconds. + * + * @var int + */ + private static $delayTimeout = 100000; + /** + * Lifetime of the lock for write in cache. + * + * Value of the variable in seconds. + * + * @var int + */ + private static $lockTimeout = 42; + /** * @var array */ @@ -76,6 +105,11 @@ class System implements ConfigTypeInterface */ private $encryptor; + /** + * @var LockManagerInterface + */ + private $locker; + /** * @param ConfigSourceInterface $source * @param PostProcessorInterface $postProcessor @@ -87,7 +121,7 @@ class System implements ConfigTypeInterface * @param string $configType * @param Reader|null $reader * @param Encryptor|null $encryptor - * + * @param LockManagerInterface|null $locker * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -101,14 +135,18 @@ public function __construct( $cachingNestedLevel = 1, $configType = self::CONFIG_TYPE, Reader $reader = null, - Encryptor $encryptor = null + Encryptor $encryptor = null, + LockManagerInterface $locker = 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->locker = $locker + ?: ObjectManager::getInstance()->get(LockManagerInterface::class); } /** @@ -187,21 +225,61 @@ private function getWithParts($path) } /** - * Load configuration data for all scopes + * Make lock on data load. * + * @param callable $dataLoader + * @param bool $flush * @return array */ - private function loadAllData() + private function lockedLoadData(callable $dataLoader, bool $flush = false): array { - $cachedData = $this->cache->load($this->configType); + $cachedData = $dataLoader(); //optimistic read - if ($cachedData === false) { - $data = $this->readData(); - } else { - $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); + while ($cachedData === false && $this->locker->isLocked(self::$lockName)) { + usleep(self::$delayTimeout); + $cachedData = $dataLoader(); } - return $data; + while ($cachedData === false) { + try { + if ($this->locker->lock(self::$lockName, self::$lockTimeout)) { + if (!$flush) { + $data = $this->readData(); + $this->cacheData($data); + $cachedData = $data; + } else { + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + $cachedData = []; + } + } + } finally { + $this->locker->unlock(self::$lockName); + } + + if ($cachedData === false) { + usleep(self::$delayTimeout); + $cachedData = $dataLoader(); + } + } + + return $cachedData; + } + + /** + * Load configuration data for all scopes + * + * @return array + */ + private function loadAllData() + { + return $this->lockedLoadData(function () { + $cachedData = $this->cache->load($this->configType); + $data = false; + if ($cachedData !== false) { + $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); + } + return $data; + }); } /** @@ -212,16 +290,14 @@ private function loadAllData() */ 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; + return $this->lockedLoadData(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; + }); } /** @@ -233,31 +309,31 @@ 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); + return $this->lockedLoadData(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; + }); } /** - * Cache configuration data + * Cache configuration data. * * Caches data per scope to avoid reading data for all scopes on every request * @@ -344,6 +420,11 @@ private function readData(): array public function clean() { $this->data = []; - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + $this->lockedLoadData( + function () { + return false; + }, + true + ); } } diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index b1074e92cc949..6bf191c20a844 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -424,6 +424,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 +525,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/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/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/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/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..87a0e666d2d7b 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,6 +90,7 @@ <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="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> </arguments> </type> <type name="Magento\Config\App\Config\Type\System\Reader"> 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/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index f98075f2294cc..46f10608bc95e 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 */ @@ -1385,7 +1386,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/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/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml index d2abfc7977519..95b86ec3a8587 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml @@ -150,4 +150,130 @@ <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> </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..c0a9f03906030 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml @@ -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/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..51bb041b66089 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml @@ -24,4 +24,58 @@ <see userInput="{{product.custom_attributes[description]}}" selector="{{StorefrontProductInfoMainSection.productDescription}}" stepKey="assertProductDescription"/> <see userInput="{{product.custom_attributes[short_description]}}" selector="{{StorefrontProductInfoMainSection.productShortDescription}}" stepKey="assertProductShortDescription"/> </actionGroup> + + <!-- 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> + + <!-- 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 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> \ No newline at end of file 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/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 6e8303e6baead..78e4c7bced8e2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml @@ -10,6 +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> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml index 9b4798c95ec72..eccff2830d2a5 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml @@ -35,9 +35,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 1df777f44f2f4..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"/> 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 c5d6abd89edbf..d1b16cb8f5ce3 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']"/> @@ -20,13 +21,21 @@ <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"/> </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/StorefrontProductInfoMainSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index b195c19f7bedd..09de3cc302903 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -11,11 +11,13 @@ <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="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/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/AdminConfigurableProductCreateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml index 24af7d44e8261..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"/> 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..e79f7f75cce3f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.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="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="MAGETWO-62808"/> + </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> +</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..981985b3f4ea8 --- /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="searchStorefrontFistChildProduct"/> + <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..e278018330aa6 --- /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="searchStorefrontFistChildProduct"/> + <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/ProductsQtyReturnAfterOrderCancelTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml index b8dfec102fdf4..03e1d1b260ffd 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml @@ -94,5 +94,7 @@ <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/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/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/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 0ae9ffde66f43..06134fcbf09d8 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -245,4 +245,7 @@ <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> </config> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml index a8712cdc183de..190ecccbfdb76 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml @@ -17,9 +17,9 @@ <legend class="legend admin__legend"> <span><?= /* @escapeNotVerified */ __('Associated Products') ?></span> </legend> - <div class="product-options"> - <div class="field admin__field _required required"> - <?php foreach ($_attributes as $_attribute): ?> + <div class="product-options fieldset admin__fieldset"> + <?php foreach ($_attributes as $_attribute): ?> + <div class="field admin__field _required required"> <label class="label admin__field-label"><?php /* @escapeNotVerified */ echo $_attribute->getProductAttribute() ->getStoreLabel($_product->getStoreId()); @@ -34,8 +34,8 @@ <option><?= /* @escapeNotVerified */ __('Choose an Option...') ?></option> </select> </div> - <?php endforeach; ?> - </div> + </div> + <?php endforeach; ?> </div> </fieldset> <script> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js index 6bbab77a3a0ab..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,11 +50,10 @@ define([ * @param {String} isConfigurable */ handlePriceValue: function (isConfigurable) { + this.disabled(!!this.isUseDefault() || isConfigurable); + if (isConfigurable) { - this.disable(); this.clear(); - } else { - this.enable(); } } }); 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 1df84d27a5c30..e732960421541 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -609,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/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..9fda4ec0173ec 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,21 @@ 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)) { + if (isset($this->parentProducts[$product->getId()])) { + return; + } + + if (!empty($this->childrenMap)) { $this->childrenMap = []; - $this->parentIds[] = $id; - } elseif (!in_array($id, $this->parentIds)) { - $this->parentIds[] = $id; } + $this->parentProducts[$product->getId()] = $product; } /** @@ -130,20 +132,23 @@ 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(); + $childCollection->addAttributeToSelect($attributeData); + /** @var Product $product */ - $product = $this->productFactory->create(); - $product->setData($linkField, $id); + $product->setData($linkField, $product->getId()); $childCollection->setProductFilter($product); /** @var Product $childProduct */ @@ -160,4 +165,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/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/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/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/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/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/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/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/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 cb0343f4ec43b..38ed688a835bc 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -289,11 +289,9 @@ protected function _extractCustomerAddressData(array & $extractedCustomerData) public function execute() { $returnToEdit = false; - $originalRequestData = $this->getRequest()->getPostValue(); - $customerId = $this->getCurrentCustomerId(); - if ($originalRequestData) { + if ($this->getRequest()->getPostValue()) { try { // optional fields might be set in request for future processing by observers in other modules $customerData = $this->_extractCustomerData(); @@ -364,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(); @@ -373,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) { @@ -489,4 +488,29 @@ private function disableAddressValidation($customer) $addressModel->setShouldIgnoreValidation(true); } } + + /** + * Retrieve formatted form data + * + * @return array + */ + private function retrieveFormattedFormData(): array + { + $originalRequestData = $this->getRequest()->getPostValue(); + + /* Customer data filtration */ + if (isset($originalRequestData['customer'])) { + $customerData = $this->_extractData( + 'adminhtml_customer', + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + [], + 'customer' + ); + + $customerData = array_intersect_key($customerData, $originalRequestData['customer']); + $originalRequestData['customer'] = array_merge($originalRequestData['customer'], $customerData); + } + + return $originalRequestData; + } } diff --git a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php index aa73e275ee0ca..f82a4d15ae8bf 100644 --- a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php +++ b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php @@ -5,10 +5,13 @@ */ namespace Magento\Customer\CustomerData\Plugin; -use Magento\Framework\Session\SessionManager; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; use Magento\Framework\Stdlib\Cookie\PhpCookieManager; +/** + * Class SessionChecker + */ class SessionChecker { /** @@ -36,10 +39,12 @@ public function __construct( /** * Delete frontend session cookie if customer session is expired * - * @param SessionManager $sessionManager + * @param SessionManagerInterface $sessionManager * @return void + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException */ - public function beforeStart(SessionManager $sessionManager) + public function beforeStart(SessionManagerInterface $sessionManager) { if (!$this->cookieManager->getCookie($sessionManager->getName()) && $this->cookieManager->getCookie('mage-cache-sessid') diff --git a/app/code/Magento/Customer/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 192b8081254f4..673300369fe06 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -815,7 +815,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); @@ -831,7 +831,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); } /** @@ -839,12 +839,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()) { @@ -917,7 +913,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; } @@ -945,12 +941,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(); @@ -960,14 +955,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); 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/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/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/Customer/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Customer/Collection.php index af8980a129d3e..394a0d3ed556d 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer/Collection.php @@ -5,6 +5,8 @@ */ namespace Magento\Customer\Model\ResourceModel\Customer; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** * Customers collection * @@ -43,6 +45,7 @@ class Collection extends \Magento\Eav\Model\Entity\Collection\VersionControl\Abs * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param string $modelName * + * @param ResourceModelPoolInterface|null $resourceModelPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -58,7 +61,8 @@ public function __construct( \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot, \Magento\Framework\DataObject\Copy\Config $fieldsetConfig, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, - $modelName = self::CUSTOMER_MODEL_NAME + $modelName = self::CUSTOMER_MODEL_NAME, + ResourceModelPoolInterface $resourceModelPool = null ) { $this->_fieldsetConfig = $fieldsetConfig; $this->_modelName = $modelName; @@ -73,7 +77,8 @@ public function __construct( $resourceHelper, $universalFactory, $entitySnapshot, - $connection + $connection, + $resourceModelPool ); } 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/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/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/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/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/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/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/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/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml index 4d531214db150..06659dae156a4 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml @@ -24,4 +24,23 @@ <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/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/OpenEditCustomerFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml index af918e8208566..14bda9ebe5b36 100755 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.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="OpenEditCustomerFromAdminActionGroup"> @@ -12,13 +13,13 @@ <argument name="customer"/> </arguments> <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> - <waitForPageLoad stepKey="waitForPageLoad1" /> + <waitForPageLoad stepKey="waitForPageLoad1"/> <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/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/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/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/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/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/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/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 da36cf722325e..5d1a900167144 100755 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -191,4 +191,84 @@ <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/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml index b3c0d8d9e0047..54644819e852b 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> @@ -178,4 +179,15 @@ <requiredEntity type="address">US_Default_Billing_Address_TX</requiredEntity> <requiredEntity type="address">US_Default_Shipping_Address_CA</requiredEntity> </entity> -</entities> + <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> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml index c1f11c9e9c390..9b6155e982013 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml @@ -13,9 +13,19 @@ <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/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/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/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/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml index 6a3687bb77c8f..71e3e673477d2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.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="AdminCustomerAccountInformationSection"> - <element name="accountInformationTab" type="button" selector="#tab_customer"/> + <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']"/> @@ -18,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/AdminCustomerFiltersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml index 02d9bc2eb5f12..25617ca05dd44 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml @@ -16,5 +16,7 @@ <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"/> </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/AdminCustomerMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml index 0a56763b66704..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,7 @@ 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> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml index 04d6c4dc2a09d..ffddc6292ef5d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml @@ -13,6 +13,8 @@ <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']" /> 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/CustomersPageSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml index 60c635387199a..93a988caf3d1c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml @@ -11,9 +11,11 @@ <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="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/StorefrontCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml index 59da4e9279a03..b819a78002c62 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml @@ -14,6 +14,10 @@ <element name="changeEmail" type="checkbox" selector="#change_email"/> <element name="changePassword" type="checkbox" selector="#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"/> + <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"/> </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 29a2f549274a7..aad9d02842271 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml @@ -17,5 +17,6 @@ <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 ee14ee5c165c5..8881a2a012ce8 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml @@ -12,6 +12,7 @@ <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"/> 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..078021db062cc --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginMessagesSection.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="StorefrontCustomerLoginMessagesSection"> + <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/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..bed0c71ae7fad 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml @@ -10,5 +10,6 @@ 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="sidebarCurrentTab" type="text" selector="//div[@id='block-collapsible-nav']//*[contains(text(), '{{var}}')]" parameterized="true"/> </section> -</sections> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml index 25c07ca9cb3c9..f52b379379ad1 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="link" 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..4d7572aedc59b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml @@ -10,10 +10,11 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontPanelHeaderSection"> <element name="WelcomeMessage" type="text" selector=".greet.welcome span"/> - <element name="createAnAccountLink" type="select" selector=".panel.header li:nth-child(3)" timeout="30"/> + <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="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/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/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/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/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/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..12e603bd3748c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontForgotPasswordTest.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="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 customer/captcha/enable 0" stepKey="disableCaptcha"/> + <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="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/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 index d1b5fb2aa7d11..d9d1c9f2e05a0 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.xml @@ -18,9 +18,6 @@ <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"/> 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/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/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php index 8d802e907a810..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 @@ -699,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]; @@ -737,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); @@ -750,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, @@ -810,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) @@ -841,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]; @@ -879,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); @@ -893,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, @@ -952,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) @@ -983,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]; @@ -1021,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); @@ -1034,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, @@ -1095,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/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/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</title> + </head> <body> <referenceBlock name="head.components"> <block class="Magento\Framework\View\Element\Js\Components" name="customer_account_create_head_components" template="Magento_Customer::js/components.phtml"/> diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml index d49dae6dee58f..3518df736c4ac 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml @@ -6,6 +6,9 @@ */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <head> + <title>Customer Login</title> + </head> <body> <referenceContainer name="content"> <!-- customer.form.login.extra --> diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerAccount.php index 030fc47d19e81..b2f524c877fd6 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerAccount.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerAccount.php @@ -11,9 +11,11 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\AuthenticationInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; /** @@ -58,6 +60,7 @@ public function __construct( * @param int|null $customerType * @return void * @throws GraphQlAuthorizationException + * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException * @throws GraphQlAuthenticationException */ @@ -74,13 +77,20 @@ public function execute(?int $customerId, ?int $customerType): void __('Customer with id "%customer_id" does not exist.', ['customer_id' => $customerId]), $e ); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); } if (true === $this->authentication->isLocked($customerId)) { throw new GraphQlAuthenticationException(__('The account is locked.')); } - $confirmationStatus = $this->accountManagement->getConfirmationStatus($customerId); + try { + $confirmationStatus = $this->accountManagement->getConfirmationStatus($customerId); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + if ($confirmationStatus === AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED) { throw new GraphQlAuthenticationException(__("This account isn't confirmed. Verify and try again.")); } 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/UpdateCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerData.php index 18510b872e64a..33f16f3d94b7d 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerData.php @@ -9,10 +9,13 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Api\DataObjectHelper; /** * Update customer data @@ -34,19 +37,35 @@ class UpdateCustomerData */ private $checkCustomerPassword; + /** + * @var DataObjectHelper + */ + private $dataObjectHelper; + + /** + * @var array + */ + private $restrictedKeys; + /** * @param CustomerRepositoryInterface $customerRepository * @param StoreManagerInterface $storeManager * @param CheckCustomerPassword $checkCustomerPassword + * @param DataObjectHelper $dataObjectHelper + * @param array $restrictedKeys */ public function __construct( CustomerRepositoryInterface $customerRepository, StoreManagerInterface $storeManager, - CheckCustomerPassword $checkCustomerPassword + CheckCustomerPassword $checkCustomerPassword, + DataObjectHelper $dataObjectHelper, + array $restrictedKeys = [] ) { $this->customerRepository = $customerRepository; $this->storeManager = $storeManager; $this->checkCustomerPassword = $checkCustomerPassword; + $this->dataObjectHelper = $dataObjectHelper; + $this->restrictedKeys = $restrictedKeys; } /** @@ -55,21 +74,16 @@ public function __construct( * @param int $customerId * @param array $data * @return void - * @throws GraphQlNoSuchEntityException - * @throws GraphQlInputException * @throws GraphQlAlreadyExistsException + * @throws GraphQlInputException + * @throws GraphQlAuthenticationException */ 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']); - } + $filteredData = array_diff_key($data, array_flip($this->restrictedKeys)); + $this->dataObjectHelper->populateWithArray($customer, $filteredData, CustomerInterface::class); if (isset($data['email']) && $customer->getEmail() !== $data['email']) { if (!isset($data['password']) || empty($data['password'])) { @@ -89,6 +103,8 @@ public function execute(int $customerId, array $data): void __('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/Resolver/IsEmailAvailable.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsEmailAvailable.php new file mode 100644 index 0000000000000..11ad0f77f8949 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsEmailAvailable.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver; + +use Magento\Customer\Api\AccountManagementInterface; +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; + +/** + * Is Customer Email Available + */ +class IsEmailAvailable implements ResolverInterface +{ + /** + * @var AccountManagementInterface + */ + private $accountManagement; + + /** + * @param AccountManagementInterface $accountManagement + */ + public function __construct( + AccountManagementInterface $accountManagement + ) { + $this->accountManagement = $accountManagement; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $email = $args['email'] ?? null; + if (!$email) { + throw new GraphQlInputException(__('"Email should be specified')); + } + $isEmailAvailable = $this->accountManagement->isEmailAvailable($email); + + return [ + 'is_email_available' => $isEmailAvailable + ]; + } +} 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 @@ +<?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\CustomerGraphQl\Model\Customer\UpdateCustomerData"> + <arguments> + <argument name="restrictedKeys" xsi:type="array"> + <item name="email" xsi:type="const">Magento\Customer\Api\Data\CustomerInterface::EMAIL</item> + </argument> + </arguments> + </type> +</config> \ 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 f4a417fe2f017..c5860005c6e0e 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -3,6 +3,9 @@ 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 { @@ -89,6 +92,7 @@ type Customer @doc(description: "Customer defines the customer name and address 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") + 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"){ @@ -126,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") 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/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/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 @@ </argument> </arguments> </type> - <type name="Magento\Deploy\App\Mode\ConfigProvider"> - <arguments> - <argument name="config" xsi:type="array"> - <item name="developer" xsi:type="array"> - <item name="production" xsi:type="array"> - <item name="dev/debug/debug_logging" xsi:type="string">0</item> - </item> - <item name="developer" xsi:type="array"> - <item name="dev/debug/debug_logging" xsi:type="null" /> - </item> - </item> - </argument> - </arguments> - </type> </config> 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 @@ <backend_model>Magento\Developer\Model\Config\Backend\AllowedIps</backend_model> </field> </group> - <group id="debug" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <field id="debug_logging" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Log to File</label> - <comment>Not available in production mode.</comment> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - </group> - <group id="syslog" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Syslog</label> - <field id="syslog_logging" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Log to Syslog</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - </group> </section> </system> </config> 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 = '<?xml version = "1.0" encoding = "UTF-8"?>' . - '<p:DCTRequest xmlns:p="http://www.dhl.com" xmlns:p1="http://www.dhl.com/datatypes" ' . - 'xmlns:p2="http://www.dhl.com/DCTRequestdatatypes" ' . + + $xmlStr = '<?xml version="1.0" encoding = "UTF-8"?>' . + '<req:DCTRequest schemaVersion="2.0" ' . + 'xmlns:req="http://www.dhl.com" ' . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' . - 'xsi:schemaLocation="http://www.dhl.com DCT-req.xsd "/>'; + '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 = '<?xml version="1.0" encoding="UTF-8"?>' . - '<req:ShipmentValidateRequest' . - $originRegion . + '<req:ShipmentRequest' . ' xmlns:req="http://www.dhl.com"' . ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' . - ' xsi:schemaLocation="http://www.dhl.com ship-val-req' . - ($originRegion ? '_' . - $originRegion : '') . - '.xsd" />'; + ' 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) '<req:KnownTrackingRequest' . ' xmlns:req="http://www.dhl.com"' . ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' . - ' xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown.xsd" />'; + ' 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('<SiteID>****</SiteID><Password>****</Password>')); - self::assertNotEmpty($this->model->collectRates($request)->getAllRates()); - self::assertContains('<Weight>18.223</Weight>', $rawPostData->getValue($this->httpClient)); - self::assertContains('<Height>0.630</Height>', $rawPostData->getValue($this->httpClient)); - self::assertContains('<Width>0.630</Width>', $rawPostData->getValue($this->httpClient)); - self::assertContains('<Depth>0.630</Depth>', $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('<Weight>18.223</Weight>', $requestXml); + self::assertContains('<Height>0.630</Height>', $requestXml); + self::assertContains('<Width>0.630</Width>', $requestXml); + self::assertContains('<Depth>0.630</Depth>', $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('<SiteID>****</SiteID><Password>****</Password>')); + + $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('<SiteID>****</SiteID><Password>****</Password>')); + 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('<Weight>0.454</Weight>', $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 @@ -<?xml version="1.0" encoding="UTF-8"?> +<?xml version="1.0"?> <!-- /** * Copyright © Magento, Inc. All rights reserved. @@ -83,7 +83,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Austria</name> <domestic>1</domestic> </AT> @@ -132,7 +132,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Belgium</name> <domestic>1</domestic> </BE> @@ -146,7 +146,7 @@ <currency>BGN</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Bulgaria</name> <domestic>1</domestic> </BG> @@ -257,7 +257,7 @@ <currency>CHF</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Switzerland</name> </CH> <CI> @@ -331,7 +331,7 @@ <currency>CZK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Czech Republic, The</name> <domestic>1</domestic> </CZ> @@ -339,7 +339,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Germany</name> <domestic>1</domestic> </DE> @@ -353,7 +353,7 @@ <currency>DKK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Denmark</name> <domestic>1</domestic> </DK> @@ -389,7 +389,7 @@ <currency>EEK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Estonia</name> <domestic>1</domestic> </EE> @@ -410,7 +410,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Spain</name> <domestic>1</domestic> </ES> @@ -424,7 +424,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Finland</name> <domestic>1</domestic> </FI> @@ -457,7 +457,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>France</name> <domestic>1</domestic> </FR> @@ -471,7 +471,7 @@ <currency>GBP</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>United Kingdom</name> <domestic>1</domestic> </GB> @@ -549,7 +549,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Greece</name> <domestic>1</domestic> </GR> @@ -612,7 +612,7 @@ <currency>HUF</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Hungary</name> <domestic>1</domestic> </HU> @@ -633,7 +633,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Ireland, Republic Of</name> <domestic>1</domestic> </IE> @@ -668,14 +668,14 @@ <currency>ISK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Iceland</name> </IS> <IT> <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Italy</name> <domestic>1</domestic> </IT> @@ -834,7 +834,7 @@ <currency>LTL</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Lithuania</name> <domestic>1</domestic> </LT> @@ -842,7 +842,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Luxembourg</name> <domestic>1</domestic> </LU> @@ -850,7 +850,7 @@ <currency>LVL</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Latvia</name> <domestic>1</domestic> </LV> @@ -1039,7 +1039,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Netherlands, The</name> <domestic>1</domestic> </NL> @@ -1047,7 +1047,7 @@ <currency>NOK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Norway</name> </NO> <NP> @@ -1127,7 +1127,7 @@ <currency>PLN</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Poland</name> <domestic>1</domestic> </PL> @@ -1142,7 +1142,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Portugal</name> <domestic>1</domestic> </PT> @@ -1177,7 +1177,7 @@ <currency>RON</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Romania</name> <domestic>1</domestic> </RO> @@ -1231,7 +1231,7 @@ <currency>SEK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Sweden</name> <domestic>1</domestic> </SE> @@ -1246,7 +1246,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Slovenia</name> <domestic>1</domestic> </SI> @@ -1254,7 +1254,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Slovakia</name> <domestic>1</domestic> </SK> 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/dhl_quote_request_data.php similarity index 92% rename from app/code/Magento/Dhl/Test/Unit/Model/_files/rates_request_data_dhl.php rename to app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_request_data.php index 32e9c78886cef..f762b99735233 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/_files/rates_request_data_dhl.php +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_request_data.php @@ -24,10 +24,9 @@ 'limit_carrier' => null, 'base_subtotal_incl_tax' => '5', 'orig_country_id' => 'US', - 'country_id' => 'US', - 'region_id' => '12', - 'city' => 'Fremont', - 'postcode' => '94538', + 'orig_region_id' => '12', + 'orig_city' => 'Fremont', + 'orig_postcode' => '94538', 'dhl_id' => 'MAGEN_8501', 'dhl_password' => 'QR2GO1U74X', 'dhl_account' => '799909537', 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/dhl_quote_response.xml similarity index 84% rename from app/code/Magento/Dhl/Test/Unit/Model/_files/success_dhl_response_rates.xml rename to app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response.xml index b529e86ef154c..7421707966411 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/_files/success_dhl_response_rates.xml +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response.xml @@ -5,9 +5,9 @@ * See COPYING.txt for license details. */ --> - -<res:DCTResponse xmlns:res="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://www.dhl.com DCT-Response.xsd"> +<res:DCTResponse xmlns:res="http://www.dhl.com" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.dhl.com DCT-Response_global-2.0.xsd"> <GetQuoteResponse> <Response> <ServiceHeader> @@ -16,15 +16,15 @@ </ServiceHeader> </Response> <BkgDetails> - <OriginServiceArea> - <FacilityCode>NUQ</FacilityCode> - <ServiceAreaCode>NUQ</ServiceAreaCode> - </OriginServiceArea> - <DestinationServiceArea> - <FacilityCode>BER</FacilityCode> - <ServiceAreaCode>BER</ServiceAreaCode> - </DestinationServiceArea> <QtdShp> + <OriginServiceArea> + <FacilityCode>NUQ</FacilityCode> + <ServiceAreaCode>NUQ</ServiceAreaCode> + </OriginServiceArea> + <DestinationServiceArea> + <FacilityCode>BER</FacilityCode> + <ServiceAreaCode>BER</ServiceAreaCode> + </DestinationServiceArea> <GlobalProductCode>E</GlobalProductCode> <LocalProductCode>E</LocalProductCode> <ProductShortName>EXPRESS 9:00</ProductShortName> @@ -42,9 +42,10 @@ <TotalTransitDays>2</TotalTransitDays> <PickupPostalLocAddDays>0</PickupPostalLocAddDays> <DeliveryPostalLocAddDays>0</DeliveryPostalLocAddDays> - <PickupNonDHLCourierCode /> - <DeliveryNonDHLCourierCode /> - <DeliveryDate>2014-01-13</DeliveryDate> + <DeliveryDate> + <DlvyDateTime>2014-01-13 11:59:00</DlvyDateTime> + <DeliveryDateTimeOffset>+00:00</DeliveryDateTimeOffset> + </DeliveryDate> <DeliveryTime>PT9H</DeliveryTime> <DimensionalWeight>2.205</DimensionalWeight> <WeightUnit>LB</WeightUnit> @@ -101,8 +102,19 @@ <TotalTaxAmount>0.000</TotalTaxAmount> <WeightChargeTax>0.000</WeightChargeTax> </QtdSInAdCur> + <PickupWindowEarliestTime>09:00:00</PickupWindowEarliestTime> + <PickupWindowLatestTime>17:00:00</PickupWindowLatestTime> + <BookingCutoffOffset>PT1H</BookingCutoffOffset> </QtdShp> <QtdShp> + <OriginServiceArea> + <FacilityCode>NUQ</FacilityCode> + <ServiceAreaCode>NUQ</ServiceAreaCode> + </OriginServiceArea> + <DestinationServiceArea> + <FacilityCode>BER</FacilityCode> + <ServiceAreaCode>BER</ServiceAreaCode> + </DestinationServiceArea> <GlobalProductCode>Q</GlobalProductCode> <LocalProductCode>Q</LocalProductCode> <ProductShortName>MEDICAL EXPRESS</ProductShortName> @@ -120,9 +132,10 @@ <TotalTransitDays>2</TotalTransitDays> <PickupPostalLocAddDays>0</PickupPostalLocAddDays> <DeliveryPostalLocAddDays>0</DeliveryPostalLocAddDays> - <PickupNonDHLCourierCode /> - <DeliveryNonDHLCourierCode /> - <DeliveryDate>2014-01-13</DeliveryDate> + <DeliveryDate> + <DlvyDateTime>2014-01-13 11:59:00</DlvyDateTime> + <DeliveryDateTimeOffset>+00:00</DeliveryDateTimeOffset> + </DeliveryDate> <DeliveryTime>PT9H</DeliveryTime> <DimensionalWeight>2.205</DimensionalWeight> <WeightUnit>LB</WeightUnit> @@ -179,8 +192,19 @@ <TotalTaxAmount>0.000</TotalTaxAmount> <WeightChargeTax>0.000</WeightChargeTax> </QtdSInAdCur> + <PickupWindowEarliestTime>09:00:00</PickupWindowEarliestTime> + <PickupWindowLatestTime>17:00:00</PickupWindowLatestTime> + <BookingCutoffOffset>PT1H</BookingCutoffOffset> </QtdShp> <QtdShp> + <OriginServiceArea> + <FacilityCode>NUQ</FacilityCode> + <ServiceAreaCode>NUQ</ServiceAreaCode> + </OriginServiceArea> + <DestinationServiceArea> + <FacilityCode>BER</FacilityCode> + <ServiceAreaCode>BER</ServiceAreaCode> + </DestinationServiceArea> <GlobalProductCode>Y</GlobalProductCode> <LocalProductCode>Y</LocalProductCode> <ProductShortName>EXPRESS 12:00</ProductShortName> @@ -198,9 +222,10 @@ <TotalTransitDays>2</TotalTransitDays> <PickupPostalLocAddDays>0</PickupPostalLocAddDays> <DeliveryPostalLocAddDays>0</DeliveryPostalLocAddDays> - <PickupNonDHLCourierCode /> - <DeliveryNonDHLCourierCode /> - <DeliveryDate>2014-01-13</DeliveryDate> + <DeliveryDate> + <DlvyDateTime>2014-01-13 11:59:00</DlvyDateTime> + <DeliveryDateTimeOffset>+00:00</DeliveryDateTimeOffset> + </DeliveryDate> <DeliveryTime>PT12H</DeliveryTime> <DimensionalWeight>2.205</DimensionalWeight> <WeightUnit>LB</WeightUnit> @@ -257,8 +282,19 @@ <TotalTaxAmount>0.000</TotalTaxAmount> <WeightChargeTax>0.000</WeightChargeTax> </QtdSInAdCur> + <PickupWindowEarliestTime>09:00:00</PickupWindowEarliestTime> + <PickupWindowLatestTime>17:00:00</PickupWindowLatestTime> + <BookingCutoffOffset>PT1H</BookingCutoffOffset> </QtdShp> <QtdShp> + <OriginServiceArea> + <FacilityCode>NUQ</FacilityCode> + <ServiceAreaCode>NUQ</ServiceAreaCode> + </OriginServiceArea> + <DestinationServiceArea> + <FacilityCode>BER</FacilityCode> + <ServiceAreaCode>BER</ServiceAreaCode> + </DestinationServiceArea> <GlobalProductCode>3</GlobalProductCode> <LocalProductCode>3</LocalProductCode> <ProductShortName>B2C</ProductShortName> @@ -275,9 +311,10 @@ <TotalTransitDays>2</TotalTransitDays> <PickupPostalLocAddDays>0</PickupPostalLocAddDays> <DeliveryPostalLocAddDays>0</DeliveryPostalLocAddDays> - <PickupNonDHLCourierCode /> - <DeliveryNonDHLCourierCode /> - <DeliveryDate>2014-01-13</DeliveryDate> + <DeliveryDate> + <DlvyDateTime>2014-01-13 11:59:00</DlvyDateTime> + <DeliveryDateTimeOffset>+00:00</DeliveryDateTimeOffset> + </DeliveryDate> <DeliveryTime>PT23H59M</DeliveryTime> <DimensionalWeight>2.205</DimensionalWeight> <WeightUnit>LB</WeightUnit> @@ -309,8 +346,19 @@ <TotalTaxAmount>0.000</TotalTaxAmount> <WeightChargeTax>0.000</WeightChargeTax> </QtdSInAdCur> + <PickupWindowEarliestTime>09:00:00</PickupWindowEarliestTime> + <PickupWindowLatestTime>17:00:00</PickupWindowLatestTime> + <BookingCutoffOffset>PT1H</BookingCutoffOffset> </QtdShp> <QtdShp> + <OriginServiceArea> + <FacilityCode>NUQ</FacilityCode> + <ServiceAreaCode>NUQ</ServiceAreaCode> + </OriginServiceArea> + <DestinationServiceArea> + <FacilityCode>BER</FacilityCode> + <ServiceAreaCode>BER</ServiceAreaCode> + </DestinationServiceArea> <GlobalProductCode>P</GlobalProductCode> <LocalProductCode>P</LocalProductCode> <ProductShortName>EXPRESS WORLDWIDE</ProductShortName> @@ -328,9 +376,10 @@ <TotalTransitDays>2</TotalTransitDays> <PickupPostalLocAddDays>0</PickupPostalLocAddDays> <DeliveryPostalLocAddDays>0</DeliveryPostalLocAddDays> - <PickupNonDHLCourierCode /> - <DeliveryNonDHLCourierCode /> - <DeliveryDate>2014-01-13</DeliveryDate> + <DeliveryDate> + <DlvyDateTime>2014-01-13 11:59:00</DlvyDateTime> + <DeliveryDateTimeOffset>+00:00</DeliveryDateTimeOffset> + </DeliveryDate> <DeliveryTime>PT23H59M</DeliveryTime> <DimensionalWeight>2.205</DimensionalWeight> <WeightUnit>LB</WeightUnit> @@ -387,6 +436,9 @@ <TotalTaxAmount>0.000</TotalTaxAmount> <WeightChargeTax>0.000</WeightChargeTax> </QtdSInAdCur> + <PickupWindowEarliestTime>09:00:00</PickupWindowEarliestTime> + <PickupWindowLatestTime>17:00:00</PickupWindowLatestTime> + <BookingCutoffOffset>PT1H</BookingCutoffOffset> </QtdShp> </BkgDetails> <Srvs> 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +return [ + [ + 'carrier' => '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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:ShipmentRequest xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.dhl.com ship-val-global-req-6.0.xsd" schemaVersion="6.0"> + <Request xmlns=""> + <ServiceHeader> + <MessageTime>currentTime</MessageTime> + <MessageReference>MAGE_SHIP_28TO32_Char_CHECKED</MessageReference> + <SiteID>some ID</SiteID> + <Password>some password</Password> + </ServiceHeader> + </Request> + <RegionCode xmlns="">CHECKED</RegionCode> + <RequestedPickupTime xmlns="">N</RequestedPickupTime> + <NewShipper xmlns="">N</NewShipper> + <LanguageCode xmlns="">EN</LanguageCode> + <PiecesEnabled xmlns="">Y</PiecesEnabled> + <Billing xmlns=""> + <ShipperAccountNumber>1234567890</ShipperAccountNumber> + <ShippingPaymentType>S</ShippingPaymentType> + <BillingAccountNumber>1234567890</BillingAccountNumber> + <DutyPaymentType>S</DutyPaymentType> + <DutyAccountNumber>1234567890</DutyAccountNumber> + </Billing> + <Consignee xmlns=""> + <CompanyName/> + <AddressLine/> + <City/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact> + <PersonName/> + <PhoneNumber/> + </Contact> + </Consignee> + <Commodity xmlns=""> + <CommodityCode>1</CommodityCode> + </Commodity> + <Reference xmlns=""> + <ReferenceID>shipment reference</ReferenceID> + <ReferenceType>St</ReferenceType> + </Reference> + <ShipmentDetails xmlns=""> + <NumberOfPieces>1</NumberOfPieces> + <Pieces xmlns=""> + <Piece xmlns=""> + <PieceID>1</PieceID> + <PackageType>CP</PackageType> + <Weight>0.454</Weight> + <Width>3</Width> + <Height>3</Height> + <Depth>3</Depth> + <PieceContents>item_name</PieceContents> + </Piece> + </Pieces> + <Weight>0.454</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode/> + <LocalProductCode/> + <Date>currentTime</Date> + <Contents>DHL Parcel</Contents> + <DoorTo>DD</DoorTo> + <DimensionUnit>C</DimensionUnit> + <PackageType>CP</PackageType> + <CurrencyCode>USD</CurrencyCode> + </ShipmentDetails> + <Shipper xmlns=""> + <ShipperID>1234567890</ShipperID> + <CompanyName/> + <RegisteredAccount>1234567890</RegisteredAccount> + <AddressLine/> + <City/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact xmlns=""> + <PersonName/> + <PhoneNumber/> + </Contact> + </Shipper> + <LabelImageFormat xmlns="">PDF</LabelImageFormat> +</req:ShipmentRequest> \ No newline at end of file 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:ShipmentRequest xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.dhl.com ship-val-global-req-6.0.xsd" schemaVersion="6.0"> + <Request xmlns=""> + <ServiceHeader> + <MessageTime>currentTime</MessageTime> + <MessageReference>MAGE_SHIP_28TO32_Char_CHECKED</MessageReference> + <SiteID>some ID</SiteID> + <Password>some password</Password> + </ServiceHeader> + </Request> + <RegionCode xmlns="">CHECKED</RegionCode> + <RequestedPickupTime xmlns="">N</RequestedPickupTime> + <NewShipper xmlns="">N</NewShipper> + <LanguageCode xmlns="">EN</LanguageCode> + <PiecesEnabled xmlns="">Y</PiecesEnabled> + <Billing xmlns=""> + <ShipperAccountNumber>1234567890</ShipperAccountNumber> + <ShippingPaymentType>S</ShippingPaymentType> + <BillingAccountNumber>1234567890</BillingAccountNumber> + <DutyPaymentType>S</DutyPaymentType> + <DutyAccountNumber>1234567890</DutyAccountNumber> + </Billing> + <Consignee xmlns=""> + <CompanyName/> + <AddressLine/> + <City/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact> + <PersonName/> + <PhoneNumber/> + </Contact> + </Consignee> + <Commodity xmlns=""> + <CommodityCode>1</CommodityCode> + </Commodity> + <Dutiable xmlns=""> + <DeclaredValue>10.00</DeclaredValue> + <DeclaredCurrency>USD</DeclaredCurrency> + </Dutiable> + <Reference xmlns=""> + <ReferenceID>shipment reference</ReferenceID> + <ReferenceType>St</ReferenceType> + </Reference> + <ShipmentDetails xmlns=""> + <NumberOfPieces>1</NumberOfPieces> + <Pieces xmlns=""> + <Piece xmlns=""> + <PieceID>1</PieceID> + <PackageType>CP</PackageType> + <Weight>0.454</Weight> + <Width>3</Width> + <Height>3</Height> + <Depth>3</Depth> + <PieceContents>item_name</PieceContents> + </Piece> + </Pieces> + <Weight>0.454</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode/> + <LocalProductCode/> + <Date>currentTime</Date> + <Contents>DHL Parcel</Contents> + <DoorTo>DD</DoorTo> + <DimensionUnit>C</DimensionUnit> + <PackageType>CP</PackageType> + <IsDutiable>Y</IsDutiable> + <CurrencyCode>USD</CurrencyCode> + </ShipmentDetails> + <Shipper xmlns=""> + <ShipperID>1234567890</ShipperID> + <CompanyName/> + <RegisteredAccount>1234567890</RegisteredAccount> + <AddressLine/> + <City/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact xmlns=""> + <PersonName/> + <PhoneNumber/> + </Contact> + </Shipper> + <LabelImageFormat xmlns="">PDF</LabelImageFormat> +</req:ShipmentRequest> diff --git a/app/code/Magento/Dhl/etc/adminhtml/system.xml b/app/code/Magento/Dhl/etc/adminhtml/system.xml index 91ed6c6568a70..37b653225c7b9 100644 --- a/app/code/Magento/Dhl/etc/adminhtml/system.xml +++ b/app/code/Magento/Dhl/etc/adminhtml/system.xml @@ -32,7 +32,8 @@ <label>Account Number</label> </field> <field id="content_type" translate="label" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> - <label>Content Type</label> + <label>Content Type (Non Domestic)</label> + <comment>Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic)</comment> <source_model>Magento\Dhl\Model\Source\Contenttype</source_model> </field> <field id="handling_type" translate="label" type="select" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> @@ -81,18 +82,12 @@ </depends> </field> <field id="doc_methods" translate="label" type="multiselect" sortOrder="170" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> - <label>Allowed Methods</label> + <label>Documents Allowed Methods</label> <source_model>Magento\Dhl\Model\Source\Method\Doc</source_model> - <depends> - <field id="content_type">D</field> - </depends> </field> <field id="nondoc_methods" translate="label" type="multiselect" sortOrder="170" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> - <label>Allowed Methods</label> + <label>Non Documents Allowed Methods</label> <source_model>Magento\Dhl\Model\Source\Method\Nondoc</source_model> - <depends> - <field id="content_type">N</field> - </depends> </field> <field id="ready_time" translate="label comment" type="text" sortOrder="180" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Ready time</label> 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 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Austria</name> <domestic>1</domestic> </AT> @@ -132,7 +132,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Belgium</name> <domestic>1</domestic> </BE> @@ -146,7 +146,7 @@ <currency>BGN</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Bulgaria</name> <domestic>1</domestic> </BG> @@ -257,7 +257,7 @@ <currency>CHF</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Switzerland</name> </CH> <CI> @@ -331,7 +331,7 @@ <currency>CZK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Czech Republic, The</name> <domestic>1</domestic> </CZ> @@ -339,7 +339,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Germany</name> <domestic>1</domestic> </DE> @@ -353,7 +353,7 @@ <currency>DKK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Denmark</name> <domestic>1</domestic> </DK> @@ -389,7 +389,7 @@ <currency>EEK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Estonia</name> <domestic>1</domestic> </EE> @@ -410,7 +410,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Spain</name> <domestic>1</domestic> </ES> @@ -424,7 +424,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Finland</name> <domestic>1</domestic> </FI> @@ -457,7 +457,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>France</name> <domestic>1</domestic> </FR> @@ -471,7 +471,7 @@ <currency>GBP</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>United Kingdom</name> <domestic>1</domestic> </GB> @@ -549,7 +549,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Greece</name> <domestic>1</domestic> </GR> @@ -612,7 +612,7 @@ <currency>HUF</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Hungary</name> <domestic>1</domestic> </HU> @@ -633,7 +633,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Ireland, Republic Of</name> <domestic>1</domestic> </IE> @@ -668,14 +668,14 @@ <currency>ISK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Iceland</name> </IS> <IT> <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Italy</name> <domestic>1</domestic> </IT> @@ -834,7 +834,7 @@ <currency>LTL</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Lithuania</name> <domestic>1</domestic> </LT> @@ -842,7 +842,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Luxembourg</name> <domestic>1</domestic> </LU> @@ -850,7 +850,7 @@ <currency>LVL</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Latvia</name> <domestic>1</domestic> </LV> @@ -1039,7 +1039,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Netherlands, The</name> <domestic>1</domestic> </NL> @@ -1047,7 +1047,7 @@ <currency>NOK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Norway</name> </NO> <NP> @@ -1127,7 +1127,7 @@ <currency>PLN</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Poland</name> <domestic>1</domestic> </PL> @@ -1142,7 +1142,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Portugal</name> <domestic>1</domestic> </PT> @@ -1177,7 +1177,7 @@ <currency>RON</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Romania</name> <domestic>1</domestic> </RO> @@ -1231,7 +1231,7 @@ <currency>SEK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Sweden</name> <domestic>1</domestic> </SE> @@ -1246,7 +1246,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Slovenia</name> <domestic>1</domestic> </SI> @@ -1254,7 +1254,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Slovakia</name> <domestic>1</domestic> </SK> 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 827a32dcea4f9..4ec34a3842fa2 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php @@ -327,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/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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DirectoryGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Directory\Api\CountryInformationAcquirerInterface; +use Magento\Directory\Api\Data\CountryInformationInterface; + +/** + * Countries field resolver, used for GraphQL request processing. + */ +class Countries implements ResolverInterface +{ + /** + * @var DataObjectProcessor + */ + private $dataProcessor; + + /** + * @var CountryInformationAcquirerInterface + */ + private $countryInformationAcquirer; + + /** + * @param DataObjectProcessor $dataProcessor + * @param CountryInformationAcquirerInterface $countryInformationAcquirer + */ + public function __construct( + DataObjectProcessor $dataProcessor, + CountryInformationAcquirerInterface $countryInformationAcquirer + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DirectoryGraphQl\Model\Resolver; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Directory\Api\CountryInformationAcquirerInterface; +use Magento\Directory\Api\Data\CountryInformationInterface; + +/** + * Country field resolver, used for GraphQL request processing. + */ +class Country implements ResolverInterface +{ + /** + * @var DataObjectProcessor + */ + private $dataProcessor; + + /** + * @var CountryInformationAcquirerInterface + */ + private $countryInformationAcquirer; + + /** + * @param DataObjectProcessor $dataProcessor + * @param CountryInformationAcquirerInterface $countryInformationAcquirer + */ + public function __construct( + DataObjectProcessor $dataProcessor, + CountryInformationAcquirerInterface $countryInformationAcquirer + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DirectoryGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Directory\Api\CurrencyInformationAcquirerInterface; +use Magento\Directory\Api\Data\CurrencyInformationInterface; + +/** + * Currency field resolver, used for GraphQL request processing. + */ +class Currency implements ResolverInterface +{ + /** + * @var DataObjectProcessor + */ + private $dataProcessor; + + /** + * @var CurrencyInformationAcquirerInterface + */ + private $currencyInformationAcquirer; + + /** + * @param DataObjectProcessor $dataProcessor + * @param CurrencyInformationAcquirerInterface $currencyInformationAcquirer + */ + public function __construct( + DataObjectProcessor $dataProcessor, + CurrencyInformationAcquirerInterface $currencyInformationAcquirer + ) { + $this->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 @@ +<?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_DirectoryGraphQl"/> +</config> diff --git a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..40ef6975fad8b --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls @@ -0,0 +1,37 @@ +# 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 + default_display_currecy_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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_DirectoryGraphQl', __DIR__); 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/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 @@ +<?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="seeDownloadableInOrder" selector="{{AdminProductDropdownOrderSection.downloadProduct}}"/> + </actionGroup> +</actionGroups> 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 @@ <data key="file">magento-logo.png</data> <data key="sample">https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg</data> </entity> + <entity name="downloadableLink1" type="downloadable_link"> + <data key="title" unique="suffix">link-1</data> + <data key="price">2.43</data> + <data key="number_of_downloads">2</data> + <data key="sample_type">url</data> + <data key="sample_url">http://example.com</data> + <data key="link_type">url</data> + <data key="link_url">http://example.com</data> + <data key="is_shareable">0</data> + <data key="sort_order">1</data> + </entity> + <entity name="downloadableLink2" type="downloadable_link"> + <data key="title" unique="suffix">link-2</data> + <data key="price">3</data> + <data key="number_of_downloads">3</data> + <data key="sample_type">url</data> + <data key="sample_url">http://example.com</data> + <data key="link_type">url</data> + <data key="link_url">http://example.com</data> + <data key="is_shareable">1</data> + <data key="sort_order">2</data> + </entity> <entity name="downloadableSampleFile" type="downloadable_sample"> <data key="title" unique="suffix">SampleFile</data> <data key="file_type">Upload File</data> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml index 6a91b60dcb588..4bed31d9f854e 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml @@ -19,6 +19,20 @@ <data key="status">1</data> <data key="urlKey" unique="suffix">downloadableproduct</data> </entity> + <entity name="DownloadableProductWithTwoLink" type="product"> + <data key="sku" unique="suffix">downloadableproduct</data> + <data key="type_id">downloadable</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">DownloadableProduct</data> + <data key="price">50.99</data> + <data key="quantity">100</data> + <data key="weight">0</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">downloadableproduct</data> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + <requiredEntity type="downloadable_link">downloadableLink1</requiredEntity> + <requiredEntity type="downloadable_link">downloadableLink2</requiredEntity> + </entity> <entity name="ApiDownloadableProduct" type="product"> <data key="sku" unique="suffix">api-downloadable-product</data> <data key="type_id">downloadable</data> 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 @@ +<?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="downloadProduct" type="text" selector="//li[not(following-sibling::li[span[@title='Bundle 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='Downloadable Product']"/> + </section> +</sections> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateDownloadableProductSwitchToSimpleTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from downloadable to simple"/> + <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/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/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/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/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index 0522ea0432176..d0a5e8de53ae9 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); @@ -1972,7 +1981,8 @@ public function afterDelete(DataObject $object) /** * Load attributes for object - * if the object will not pass all attributes for this entity type will be loaded + * + * If the object will not pass all attributes for this entity type will be loaded * * @param array $attributes * @param AbstractEntity|null $object diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php index 0991b3f9f4b23..56188ab997b76 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php @@ -80,6 +80,8 @@ public function getOptionText($value) } /** + * Get option id. + * * @param string $value * @return null|string */ diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php new file mode 100644 index 0000000000000..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..dad420ea0b375 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -6,10 +6,12 @@ namespace Magento\Eav\Model\Entity\Collection; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection\SourceProviderInterface; use Magento\Framework\Data\Collection\AbstractDb; use Magento\Framework\DB\Select; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; /** * Entity/Attribute/Model - collection abstract @@ -125,9 +127,15 @@ abstract class AbstractCollection extends AbstractDb implements SourceProviderIn protected $_resourceHelper; /** + * @deprecated To instantiate resource models, use $resourceModelPool instead + * * @var \Magento\Framework\Validator\UniversalFactory */ protected $_universalFactory; + /** + * @var ResourceModelPoolInterface + */ + private $resourceModelPool; /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory @@ -140,6 +148,7 @@ abstract class AbstractCollection extends AbstractDb implements SourceProviderIn * @param \Magento\Eav\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param mixed $connection + * @param ResourceModelPoolInterface|null $resourceModelPool * @codeCoverageIgnore * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -152,8 +161,9 @@ public function __construct( \Magento\Framework\App\ResourceConnection $resource, \Magento\Eav\Model\EntityFactory $eavEntityFactory, \Magento\Eav\Model\ResourceModel\Helper $resourceHelper, - \Magento\Framework\Validator\UniversalFactory $universalFactory, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null + \Magento\Framework\Validator\UniversalFactory $universalFactory = null, + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ResourceModelPoolInterface $resourceModelPool = null ) { $this->_eventManager = $eventManager; $this->_eavConfig = $eavConfig; @@ -161,6 +171,12 @@ public function __construct( $this->_eavEntityFactory = $eavEntityFactory; $this->_resourceHelper = $resourceHelper; $this->_universalFactory = $universalFactory; + if ($resourceModelPool === null) { + $resourceModelPool = ObjectManager::getInstance()->get( + ResourceModelPoolInterface::class + ); + } + $this->resourceModelPool = $resourceModelPool; parent::__construct($entityFactory, $logger, $fetchStrategy, $connection); $this->_construct(); $this->setConnection($this->getEntity()->getConnection()); @@ -227,7 +243,7 @@ protected function _initSelect() protected function _init($model, $entityModel) { $this->setItemObjectClass($model); - $entity = $this->_universalFactory->create($entityModel); + $entity = $this->resourceModelPool->get($entityModel); $this->setEntity($entity); return $this; @@ -399,7 +415,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'); } /** 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..2181c6bc1be05 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php @@ -5,10 +5,13 @@ */ namespace Magento\Eav\Model\Entity\Collection\VersionControl; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** * Class Abstract Collection * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\AbstractCollection { @@ -27,8 +30,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 \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot , * @param mixed $connection + * @param ResourceModelPoolInterface|null $resourceModelPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @codeCoverageIgnore */ @@ -43,7 +47,8 @@ public function __construct( \Magento\Eav\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ResourceModelPoolInterface $resourceModelPool = null ) { $this->entitySnapshot = $entitySnapshot; @@ -57,7 +62,8 @@ public function __construct( $eavEntityFactory, $resourceHelper, $universalFactory, - $connection + $connection, + $resourceModelPool ); } diff --git a/app/code/Magento/Eav/Model/Entity/Type.php b/app/code/Magento/Eav/Model/Entity/Type.php index 444d58bf546d4..b24f86c73e8df 100644 --- a/app/code/Magento/Eav/Model/Entity/Type.php +++ b/app/code/Magento/Eav/Model/Entity/Type.php @@ -5,6 +5,9 @@ */ namespace Magento\Eav\Model\Entity; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** * Entity type model * @@ -75,10 +78,16 @@ class Type extends \Magento\Framework\Model\AbstractModel protected $_storeFactory; /** + * @deprecated To instantiate resource models, use $resourceModelPool instead * @var \Magento\Framework\Validator\UniversalFactory */ protected $_universalFactory; + /** + * @var ResourceModelPoolInterface + */ + private $resourceModelPool; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -89,7 +98,9 @@ class Type extends \Magento\Framework\Model\AbstractModel * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param ResourceModelPoolInterface|null $resourceModelPool * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -100,13 +111,20 @@ public function __construct( \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ResourceModelPoolInterface $resourceModelPool = null ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->_attributeFactory = $attributeFactory; $this->_attSetFactory = $attSetFactory; $this->_storeFactory = $storeFactory; $this->_universalFactory = $universalFactory; + if ($resourceModelPool === null) { + $resourceModelPool = ObjectManager::getInstance()->get( + ResourceModelPoolInterface::class + ); + } + $this->resourceModelPool = $resourceModelPool; } /** @@ -363,7 +381,7 @@ public function getAttributeModel() */ public function getEntity() { - return $this->_universalFactory->create($this->_data['entity_model']); + return $this->resourceModelPool->get($this->_data['entity_model']); } /** 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/Test/Unit/Model/Entity/Collection/AbstractCollectionTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/AbstractCollectionTest.php index bc4ed7d4bd9e4..c7af666604b39 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,9 +61,9 @@ 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; + protected $resourceModelPoolMock; /** * @var \Magento\Framework\DB\Statement\Pdo\Mysql|\PHPUnit_Framework_MockObject_MockObject @@ -71,17 +74,11 @@ protected function setUp() { $this->coreEntityFactoryMock = $this->createMock(\Magento\Framework\Data\Collection\EntityFactory::class); $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->fetchStrategyMock = $this->createMock( - \Magento\Framework\Data\Collection\Db\FetchStrategyInterface::class - ); + $this->fetchStrategyMock = $this->createMock(FetchStrategyInterface::class); $this->eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); $this->configMock = $this->createMock(\Magento\Eav\Model\Config::class); - $this->coreResourceMock = $this->createMock(\Magento\Framework\App\ResourceConnection::class); $this->resourceHelperMock = $this->createMock(\Magento\Eav\Model\ResourceModel\Helper::class); - $this->validatorFactoryMock = $this->createMock(\Magento\Framework\Validator\UniversalFactory::class); $this->entityFactoryMock = $this->createMock(\Magento\Eav\Model\EntityFactory::class); - /** @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ - $connectionMock = $this->createMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class); $this->statementMock = $this->createPartialMock(\Magento\Framework\DB\Statement\Pdo\Mysql::class, ['fetch']); /** @var $selectMock \Magento\Framework\DB\Select|\PHPUnit_Framework_MockObject_MockObject */ $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); @@ -92,9 +89,12 @@ protected function setUp() )->will( $this->returnCallback([$this, 'getMagentoObject']) ); + /** @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ + $connectionMock = $this->createMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class); $connectionMock->expects($this->any())->method('select')->will($this->returnValue($selectMock)); $connectionMock->expects($this->any())->method('query')->willReturn($this->statementMock); + $this->coreResourceMock = $this->createMock(\Magento\Framework\App\ResourceConnection::class); $this->coreResourceMock->expects( $this->any() )->method( @@ -106,10 +106,11 @@ protected function setUp() $entityMock->expects($this->any())->method('getConnection')->will($this->returnValue($connectionMock)); $entityMock->expects($this->any())->method('getDefaultAttributes')->will($this->returnValue([])); - $this->validatorFactoryMock->expects( + $this->resourceModelPoolMock = $this->createMock(ResourceModelPoolInterface::class); + $this->resourceModelPoolMock->expects( $this->any() )->method( - 'create' + 'get' )->with( 'test_entity_model' // see \Magento\Eav\Test\Unit\Model\Entity\Collection\AbstractCollectionStub )->will( @@ -125,8 +126,9 @@ protected function setUp() $this->coreResourceMock, $this->entityFactoryMock, $this->resourceHelperMock, - $this->validatorFactoryMock, - null + null, + null, + $this->resourceModelPoolMock ); } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php index cce7b43786a76..5b41b9b71f4b5 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php @@ -39,7 +39,7 @@ protected function setUp() \Magento\Eav\Test\Unit\Model\Entity\Collection\VersionControl\AbstractCollectionStub::class, [ 'entityFactory' => $this->coreEntityFactoryMock, - 'universalFactory' => $this->validatorFactoryMock, + 'resourceModelPool' => $this->resourceModelPoolMock, 'entitySnapshot' => $this->entitySnapshot ] ); diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index 8e897b979d2f0..a4c89dcfab2af 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" /> 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/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..181cbd4dfd4b3 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,6 +134,7 @@ public function getFields(array $context = []): array ]; } + $searchCriteria = $this->searchCriteriaBuilder->create(); $groups = $this->groupRepository->getList($searchCriteria)->getItems(); $priceAttribute = $this->attributeAdapterProvider->getByAttributeCode('price'); foreach ($groups as $group) { 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/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..255c7885e84b9 --- /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($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..0c03a9df18dc8 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, 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..aaa9d8a88382f 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php @@ -10,6 +10,9 @@ use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; +/** + * Builder for match query. + */ class Match implements QueryInterface { /** @@ -40,7 +43,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function build(array $selectQuery, RequestQueryInterface $requestQuery, $conditionType) { @@ -61,6 +64,8 @@ public function build(array $selectQuery, RequestQueryInterface $requestQuery, $ } /** + * Prepare query. + * * @param string $queryValue * @param string $conditionType * @return array @@ -124,11 +129,11 @@ 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. * + * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. + * https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs. + * * @param string $value * @return string */ 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/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/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 7e219bb2f918f..9796dc93858f1 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -13,6 +13,116 @@ <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" /> + + <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 +131,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 +141,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" /> @@ -360,6 +470,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"> @@ -417,7 +528,7 @@ <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> 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/Config.php b/app/code/Magento/Elasticsearch6/Model/Config.php new file mode 100644 index 0000000000000..1a989e2705fdd --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Model/Config.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch6\Model; + +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\AdvancedSearch\Model\Client\ClientResolver; + +/** + * Elasticsearch6 config model + */ +class Config extends \Magento\Elasticsearch\Model\Config +{ + /** + * Search engine name + */ + private const ENGINE_NAME_6 = 'elasticsearch6'; + + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * Constructor + * + * @param ScopeConfigInterface $scopeConfig + * @param ClientResolver|null $clientResolver + * @param EngineResolverInterface|null $engineResolver + * @param string|null $prefix + */ + public function __construct( + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\AdvancedSearch\Model\Client\ClientResolver $clientResolver, + \Magento\Framework\Search\EngineResolverInterface $engineResolver, + $prefix = null + ) { + parent::__construct($scopeConfig, $clientResolver, $engineResolver, $prefix); + $this->engineResolver = $engineResolver; + } + + /** + * Return true if third party search engine is used + * + * @return bool + */ + public function isElasticsearchEnabled() + { + return in_array($this->engineResolver->getCurrentSearchEngine(), [self::ENGINE_NAME_6]); + } +} 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..77e1270f54fc2 --- /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\Elasticsearch6\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..957edc559fdcb --- /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\Elasticsearch6\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..25eff42fd3442 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/etc/di.xml @@ -0,0 +1,165 @@ +<?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\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\Elasticsearch6\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">2147483647</item> + </argument> + </arguments> + </type> +</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/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/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/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php index 8d1548036cd3e..251dca8ef1615 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 @@ -7,7 +7,16 @@ */ namespace Magento\GroupedProduct\Model\ResourceModel\Product\Type\Grouped; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** + * Associated products collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AssociatedProductsCollection extends \Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection @@ -52,6 +61,12 @@ class AssociatedProductsCollection extends \Magento\Catalog\Model\ResourceModel\ * @param \Magento\Catalog\Model\ProductTypes\ConfigInterface $config * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * + * @param ProductLimitationFactory|null $productLimitationFactory + * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory + * @param ResourceModelPoolInterface|null $resourceModelPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -76,7 +91,13 @@ public function __construct( \Magento\Customer\Api\GroupManagementInterface $groupManagement, \Magento\Framework\Registry $coreRegistry, \Magento\Catalog\Model\ProductTypes\ConfigInterface $config, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ProductLimitationFactory $productLimitationFactory = null, + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null, + ResourceModelPoolInterface $resourceModelPool = null ) { $this->_coreRegistry = $coreRegistry; $this->_config = $config; @@ -100,7 +121,13 @@ public function __construct( $customerSession, $dateTime, $groupManagement, - $connection + $connection, + $productLimitationFactory, + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $resourceModelPool ); } 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/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/Ui/DataProvider/Product/Form/Modifier/Grouped.php b/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php index 57d9bc78aaf28..fff84d9221c8a 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 @@ -414,8 +414,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 +423,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/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 1cc8dc224acb6..d115aea7f2ff9 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -312,7 +312,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 @@ -630,16 +630,7 @@ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource $messages = $this->getOperationResultMessages($errorAggregator); $this->addLogComment($messages); - $errorsCount = $errorAggregator->getErrorsCount(); - $result = !$errorsCount; - $validationStrategy = $this->getData(self::FIELD_NAME_VALIDATION_STRATEGY); - if ($errorsCount - && $validationStrategy === ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS - ) { - $this->messageManager->addWarningMessage(__('Skipped errors: %1', $errorsCount)); - $result = true; - } - + $result = !$errorAggregator->isErrorLimitExceeded(); if ($result) { $this->addLogComment(__('Import data validation is complete.')); } 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/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..ad9e7672ce11a --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.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="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"/> + </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/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 8f7955e679cc2..04ee726349123 100644 --- a/app/code/Magento/ImportExport/etc/adminhtml/di.xml +++ b/app/code/Magento/ImportExport/etc/adminhtml/di.xml @@ -11,4 +11,19 @@ <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/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/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/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..3d1e5ef0b8e6c --- /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/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/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/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/Test/VerifySubscribedNewsletterDisplayedTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml index fde7daacebd5e..22ca214c94aec 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml @@ -47,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> 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/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/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/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/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/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/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/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/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/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/Transparent/RequestSecureToken.php b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php index 847388eb755a1..f4b4c39ca4021 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); } 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/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/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/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/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/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/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/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/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..425e4cffb666c 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 @@ -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..ad07d642de127 100644 --- a/app/code/Magento/Paypal/i18n/en_US.csv +++ b/app/code/Magento/Paypal/i18n/en_US.csv @@ -697,3 +697,38 @@ 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>. " +"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" \ No newline at end of file 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/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/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/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 17d3b9205625f..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); diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index 6ed8393f80658..8f216b64aa9b0 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) */ @@ -356,6 +357,13 @@ public function placeOrder($cartId, PaymentInterface $paymentMethod = null) if ($quote->getCheckoutMethod() === self::METHOD_GUEST) { $quote->setCustomerId(null); $quote->setCustomerEmail($quote->getBillingAddress()->getEmail()); + if ($quote->getCustomerFirstname() === null && $quote->getCustomerLastname() === null) { + $quote->setCustomerFirstname($quote->getBillingAddress()->getFirstname()); + $quote->setCustomerLastname($quote->getBillingAddress()->getLastname()); + if ($quote->getBillingAddress()->getMiddlename() === null) { + $quote->setCustomerMiddlename($quote->getBillingAddress()->getMiddlename()); + } + } $quote->setCustomerIsGuest(true); $quote->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID); } diff --git a/app/code/Magento/Quote/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/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/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php index 72e516e35cd6e..b61f95b4eee6c 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php @@ -645,7 +645,7 @@ public function testPlaceOrderIfCustomerIsGuest() $addressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getEmail']); $addressMock->expects($this->once())->method('getEmail')->willReturn($email); - $this->quoteMock->expects($this->once())->method('getBillingAddress')->with()->willReturn($addressMock); + $this->quoteMock->expects($this->any())->method('getBillingAddress')->with()->willReturn($addressMock); $this->quoteMock->expects($this->once())->method('setCustomerIsGuest')->with(true)->willReturnSelf(); $this->quoteMock->expects($this->once()) diff --git a/app/code/Magento/Quote/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/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..1b32866ed883c 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -61,6 +61,7 @@ public function __construct( * @return void * @throws GraphQlNoSuchEntityException * @throws GraphQlInputException + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute(Quote $cart, array $cartItemData): void { @@ -74,7 +75,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/ExtractDataFromAddress.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromAddress.php new file mode 100644 index 0000000000000..20212d3412595 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromAddress.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 the necessary address fields from an Address model + */ +class ExtractDataFromAddress +{ + /** + * @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/ExtractDataFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromCart.php deleted file mode 100644 index 438f28980918d..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromCart.php +++ /dev/null @@ -1,50 +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, - ]; - } - - $appliedCoupon = $cart->getCouponCode(); - - return [ - 'items' => $items, - 'applied_coupon' => $appliedCoupon ? ['code' => $appliedCoupon] : null - ]; - } -} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php index c3207bf478bbe..21df2271cc7f3 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php @@ -45,12 +45,12 @@ public function __construct( * 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 +69,14 @@ public function execute(string $cartHash, ?int $userId): Quote ); } - $customerId = (int)$cart->getCustomerId(); + $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/GetCustomerAddress.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCustomerAddress.php new file mode 100644 index 0000000000000..039324abf6854 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCustomerAddress.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\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; + +/** + * Get customer address + */ +class GetCustomerAddress +{ + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @param AddressRepositoryInterface $addressRepository + */ + public function __construct(AddressRepositoryInterface $addressRepository) + { + $this->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( + __( + 'The current user cannot use address with ID "%address_id"', + ['address_id' => $addressId] + ) + ); + } + return $customerAddress; + } +} 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..1fb737d964139 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetQuoteAddress.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\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +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 int $quoteAddressId + * @param int|null $customerId + * @return AddressInterface + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + * @throws GraphQlAuthorizationException + */ + public function execute(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]) + ); + } + + $quoteAddressCustomerId = (int)$quoteAddress->getCustomerId(); + + /* Guest cart, allow operations */ + if (!$quoteAddressCustomerId && null === $customerId) { + return $quoteAddress; + } + + if ($quoteAddressCustomerId !== $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..76bdc74611131 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php @@ -0,0 +1,67 @@ +<?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 as CustomerAddress; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Model\Quote\AddressFactory as BaseQuoteAddressFactory; + +/** + * Create QuoteAddress + */ +class QuoteAddressFactory +{ + /** + * @var BaseQuoteAddressFactory + */ + private $quoteAddressFactory; + + /** + * @param BaseQuoteAddressFactory $quoteAddressFactory + */ + public function __construct( + BaseQuoteAddressFactory $quoteAddressFactory + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + } + + /** + * Create QuoteAddress based on input data + * + * @param array $addressInput + * @return QuoteAddress + */ + public function createBasedOnInputData(array $addressInput): QuoteAddress + { + $addressInput['country_id'] = $addressInput['country_code'] ?? ''; + + $quoteAddress = $this->quoteAddressFactory->create(); + $quoteAddress->addData($addressInput); + return $quoteAddress; + } + + /** + * Create QuoteAddress based on CustomerAddress + * + * @param CustomerAddress $customerAddress + * @return QuoteAddress + * @throws GraphQlInputException + */ + public function createBasedOnCustomerAddress(CustomerAddress $customerAddress): QuoteAddress + { + $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..cf277c729cdfd --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php @@ -0,0 +1,109 @@ +<?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\CheckCustomerAccount; +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 CheckCustomerAccount + */ + private $checkCustomerAccount; + + /** + * @var GetCustomerAddress + */ + private $getCustomerAddress; + + /** + * @var AssignBillingAddressToCart + */ + private $assignBillingAddressToCart; + + /** + * @param QuoteAddressFactory $quoteAddressFactory + * @param CheckCustomerAccount $checkCustomerAccount + * @param GetCustomerAddress $getCustomerAddress + * @param AssignBillingAddressToCart $assignBillingAddressToCart + */ + public function __construct( + QuoteAddressFactory $quoteAddressFactory, + CheckCustomerAccount $checkCustomerAccount, + GetCustomerAddress $getCustomerAddress, + AssignBillingAddressToCart $assignBillingAddressToCart + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->checkCustomerAccount = $checkCustomerAccount; + $this->getCustomerAddress = $getCustomerAddress; + $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 { + $this->checkCustomerAccount->execute($context->getUserId(), $context->getUserType()); + $customerAddress = $this->getCustomerAddress->execute((int)$customerAddressId, (int)$context->getUserId()); + $billingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress($customerAddress); + } + + $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..8e54ab0d3feef --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -0,0 +1,94 @@ +<?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\CheckCustomerAccount; +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 CheckCustomerAccount + */ + private $checkCustomerAccount; + + /** + * @var GetCustomerAddress + */ + private $getCustomerAddress; + + /** + * @var AssignShippingAddressToCart + */ + private $assignShippingAddressToCart; + + /** + * @param QuoteAddressFactory $quoteAddressFactory + * @param CheckCustomerAccount $checkCustomerAccount + * @param GetCustomerAddress $getCustomerAddress + * @param AssignShippingAddressToCart $assignShippingAddressToCart + */ + public function __construct( + QuoteAddressFactory $quoteAddressFactory, + CheckCustomerAccount $checkCustomerAccount, + GetCustomerAddress $getCustomerAddress, + AssignShippingAddressToCart $assignShippingAddressToCart + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->checkCustomerAccount = $checkCustomerAccount; + $this->getCustomerAddress = $getCustomerAddress; + $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 { + $this->checkCustomerAccount->execute($context->getUserId(), $context->getUserType()); + $customerAddress = $this->getCustomerAddress->execute((int)$customerAddressId, (int)$context->getUserId()); + $shippingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress($customerAddress); + } + + $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..37e0118423745 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php @@ -0,0 +1,76 @@ +<?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\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($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..ca69b763929be --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupon.php @@ -0,0 +1,34 @@ +<?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 AppliedCoupon 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']; + + $appliedCoupon = $cart->getCouponCode(); + + 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..5429d5333b25b 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,16 @@ 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) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $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..a03533ecefffa --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.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\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\ExtractDataFromAddress; + +/** + * @inheritdoc + */ +class BillingAddress implements ResolverInterface +{ + /** + * @var ExtractDataFromAddress + */ + private $extractDataFromAddress; + + /** + * @param ExtractDataFromAddress $extractDataFromAddress + */ + public function __construct(ExtractDataFromAddress $extractDataFromAddress) + { + $this->extractDataFromAddress = $extractDataFromAddress; + } + + /** + * @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']; + + $billingAddress = $cart->getBillingAddress(); + if (null === $billingAddress) { + return null; + } + + $addressData = $this->extractDataFromAddress->execute($billingAddress); + return $addressData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart.php index 5023c186f1e6c..db74b1fa4174b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart.php @@ -12,18 +12,12 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; -use Magento\QuoteGraphQl\Model\Cart\ExtractDataFromCart; /** * @inheritdoc */ class Cart implements ResolverInterface { - /** - * @var ExtractDataFromCart - */ - private $extractDataFromCart; - /** * @var GetCartForUser */ @@ -31,14 +25,11 @@ class Cart implements ResolverInterface /** * @param GetCartForUser $getCartForUser - * @param ExtractDataFromCart $extractDataFromCart */ public function __construct( - GetCartForUser $getCartForUser, - ExtractDataFromCart $extractDataFromCart + GetCartForUser $getCartForUser ) { $this->getCartForUser = $getCartForUser; - $this->extractDataFromCart = $extractDataFromCart; } /** @@ -46,7 +37,7 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (!isset($args['cart_id'])) { + if (!isset($args['cart_id']) || empty($args['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } $maskedCartId = $args['cart_id']; @@ -54,14 +45,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $currentUserId = $context->getUserId(); $cart = $this->getCartForUser->execute($maskedCartId, $currentUserId); - $data = array_merge( - [ - 'cart_id' => $maskedCartId, - 'model' => $cart - ], - $this->extractDataFromCart->execute($cart) - ); - - return $data; + return [ + 'model' => $cart, + ]; } } 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/RemoveCouponFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php index c21d869ddac7d..5a708ceaedc28 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,16 @@ 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) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $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/CartAddresses.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SelectedPaymentMethod.php similarity index 60% rename from app/code/Magento/QuoteGraphQl/Model/Resolver/CartAddresses.php rename to app/code/Magento/QuoteGraphQl/Model/Resolver/SelectedPaymentMethod.php index 69544672bf12e..7a99b04638ac3 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartAddresses.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SelectedPaymentMethod.php @@ -11,27 +11,12 @@ 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 +class SelectedPaymentMethod implements ResolverInterface { - /** - * @var AddressDataProvider - */ - private $addressDataProvider; - - /** - * @param AddressDataProvider $addressDataProvider - */ - public function __construct( - AddressDataProvider $addressDataProvider - ) { - $this->addressDataProvider = $addressDataProvider; - } - /** * @inheritdoc */ @@ -41,8 +26,17 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value throw new LocalizedException(__('"model" value should be specified')); } + /** @var \Magento\Quote\Model\Quote $cart */ $cart = $value['model']; - return $this->addressDataProvider->getCartAddresses($cart); + $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/SetPaymentMethodOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php new file mode 100644 index 0000000000000..a93c8032c996a --- /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 "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..3a55ef9ae25a8 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.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\QuoteGraphQl\Model\Cart\ExtractDataFromAddress; + +/** + * @inheritdoc + */ +class ShippingAddresses implements ResolverInterface +{ + /** + * @var ExtractDataFromAddress + */ + private $extractDataFromAddress; + + /** + * @param ExtractDataFromAddress $extractDataFromAddress + */ + public function __construct(ExtractDataFromAddress $extractDataFromAddress) + { + $this->extractDataFromAddress = $extractDataFromAddress; + } + + /** + * @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']; + + $addressesData = []; + $shippingAddresses = $cart->getAllShippingAddresses(); + + if (count($shippingAddresses)) { + foreach ($shippingAddresses as $shippingAddress) { + $addressesData[] = $this->extractDataFromAddress->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/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 4c1101a5f90a8..79cea3855f6f3 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -2,30 +2,61 @@ # See COPYING.txt for license details. type Query { - Cart(cart_id: String!): Cart @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Cart") @doc(description:"Returns information about shopping cart") + 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") + 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") } -input SetShippingAddressesOnCartInput { +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 +64,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,38 +105,39 @@ 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 SetPaymentMethodOnCartInput { + cart_id: String! + payment_method: PaymentMethodInput! } -type SetShippingAddressesOnCartOutput { +input PaymentMethodInput { + code: String! @doc(description:"Payment method code") + purchase_order_number: String @doc(description:"Purchase order number") +} + +type SetPaymentMethodOnCartOutput { cart: Cart! } -type SetShippingMethodsOnCartOutput { +type SetBillingAddressOnCartOutput { cart: Cart! } -# 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 { - cart_id: String! - customer_address_id: Int - address: CartAddressInput +type SetShippingAddressesOnCartOutput { + cart: Cart! } -input ApplyCouponToCartInput { - cart_id: String! - coupon_code: String! +type SetShippingMethodsOnCartOutput { + cart: Cart! } type ApplyCouponToCartOutput { @@ -94,13 +145,16 @@ type ApplyCouponToCartOutput { } 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") + 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") } type CartAddress { + address_id: Int firstname: String lastname: String company: String @@ -111,15 +165,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! } @@ -133,12 +187,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 { @@ -158,22 +233,19 @@ 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! } @@ -181,9 +253,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") { 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..aa01e33caf3d2 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php @@ -4,12 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Customers Report collection - */ namespace Magento\Reports\Model\ResourceModel\Customer; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** + * Customers Report collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -91,6 +92,7 @@ class Collection extends \Magento\Customer\Model\ResourceModel\Customer\Collecti * @param mixed $connection * @param string $modelName * + * @param ResourceModelPoolInterface|null $resourceModelPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -109,7 +111,8 @@ public function __construct( \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory $quoteItemFactory, \Magento\Sales\Model\ResourceModel\Order\Collection $orderResource, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, - $modelName = self::CUSTOMER_MODEL_NAME + $modelName = self::CUSTOMER_MODEL_NAME, + ResourceModelPoolInterface $resourceModelPool = null ) { parent::__construct( $entityFactory, @@ -124,7 +127,8 @@ public function __construct( $entitySnapshot, $fieldsetConfig, $connection, - $modelName + $modelName, + $resourceModelPool ); $this->orderResource = $orderResource; $this->quoteRepository = $quoteRepository; diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index fd9adbe734101..da7ab97a1b211 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -445,7 +445,7 @@ public function getDateRange($range, $customStart, $customEnd, $returnObjects = break; case 'custom': - $dateStart = $customStart ? $customStart : $dateEnd; + $dateStart = $customStart ? $customStart : $dateStart; $dateEnd = $customEnd ? $customEnd : $dateEnd; break; diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php index 337c87f6da03d..451007960a1ce 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php @@ -5,13 +5,20 @@ */ /** - * Products Report collection - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Reports\Model\ResourceModel\Product; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** + * Products Report collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -89,7 +96,13 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\Product\Type $productType * @param \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteResource * @param mixed $connection - * + * @param ProductLimitationFactory|null $productLimitationFactory + * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory + * @param ResourceModelPoolInterface $resourceModelPool + * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -116,7 +129,13 @@ public function __construct( \Magento\Reports\Model\Event\TypeFactory $eventTypeFactory, \Magento\Catalog\Model\Product\Type $productType, \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteResource, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ProductLimitationFactory $productLimitationFactory = null, + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null, + ResourceModelPoolInterface $resourceModelPool = null ) { $this->setProductEntityId($product->getEntityIdField()); $this->setProductEntityTableName($product->getEntityTable()); @@ -141,7 +160,13 @@ public function __construct( $customerSession, $dateTime, $groupManagement, - $connection + $connection, + $productLimitationFactory, + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $resourceModelPool ); $this->_eventTypeFactory = $eventTypeFactory; $this->_productType = $productType; @@ -149,7 +174,8 @@ public function __construct( } /** - * Set Type for COUNT SQL Select + * Set Type for COUNT SQL Select. + * * @codeCoverageIgnore * * @param int $type @@ -162,7 +188,8 @@ public function setSelectCountSqlType($type) } /** - * Set product entity id + * Set product entity id. + * * @codeCoverageIgnore * * @param string $entityId @@ -175,7 +202,8 @@ public function setProductEntityId($entityId) } /** - * Get product entity id + * Get product entity id. + * * @codeCoverageIgnore * * @return int @@ -186,7 +214,8 @@ public function getProductEntityId() } /** - * Set product entity table name + * Set product entity table name. + * * @codeCoverageIgnore * * @param string $value @@ -199,7 +228,8 @@ public function setProductEntityTableName($value) } /** - * Get product entity table name + * Get product entity table name. + * * @codeCoverageIgnore * * @return string @@ -210,7 +240,8 @@ public function getProductEntityTableName() } /** - * Get product attribute set id + * Get product attribute set id. + * * @codeCoverageIgnore * * @return int @@ -221,7 +252,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/Index/Collection/AbstractCollection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php index 7371bc4359f46..bec8faaee0ca7 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,13 +5,20 @@ */ /** - * Reports Product Index Abstract Product Resource Collection - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Reports\Model\ResourceModel\Product\Index\Collection; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** + * Reports Product Index Abstract Product Resource Collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -53,7 +60,12 @@ abstract class AbstractCollection extends \Magento\Catalog\Model\ResourceModel\P * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement * @param \Magento\Customer\Model\Visitor $customerVisitor * @param mixed $connection - * + * @param ProductLimitationFactory|null $productLimitationFactory + * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory + * @param ResourceModelPoolInterface|null $resourceModelPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -77,7 +89,13 @@ public function __construct( \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Customer\Api\GroupManagementInterface $groupManagement, \Magento\Customer\Model\Visitor $customerVisitor, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ProductLimitationFactory $productLimitationFactory = null, + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null, + ResourceModelPoolInterface $resourceModelPool = null ) { parent::__construct( $entityFactory, @@ -99,7 +117,13 @@ public function __construct( $customerSession, $dateTime, $groupManagement, - $connection + $connection, + $productLimitationFactory, + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $resourceModelPool ); $this->_customerVisitor = $customerVisitor; } @@ -181,7 +205,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..8bf50f4c1b8e7 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php @@ -5,15 +5,21 @@ */ /** - * Product Low Stock Report Collection - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Reports\Model\ResourceModel\Product\Lowstock; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; /** + * Product Low Stock Report Collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -78,7 +84,13 @@ class Collection extends \Magento\Reports\Model\ResourceModel\Product\Collection * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration * @param \Magento\CatalogInventory\Model\ResourceModel\Stock\Item $itemResource * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection - * + * @param ProductLimitationFactory|null $productLimitationFactory + * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory + * @param ResourceModelPoolInterface|null $resourceModelPool + * @throws LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -108,7 +120,13 @@ public function __construct( \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, \Magento\CatalogInventory\Model\ResourceModel\Stock\Item $itemResource, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ProductLimitationFactory $productLimitationFactory = null, + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null, + ResourceModelPoolInterface $resourceModelPool = null ) { parent::__construct( $entityFactory, @@ -134,7 +152,13 @@ public function __construct( $eventTypeFactory, $productType, $quoteResource, - $connection + $connection, + $productLimitationFactory, + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $resourceModelPool ); $this->stockRegistry = $stockRegistry; $this->stockConfiguration = $stockConfiguration; diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml index b96a367903ac8..d367b2deb5922 100644 --- a/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml @@ -19,4 +19,17 @@ <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/Section/OrderReportMainSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection.xml index fb71a67da87dc..7ad9bdfa8c12c 100644 --- a/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection.xml +++ b/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection.xml @@ -19,6 +19,7 @@ <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"> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml index 38ad418865eac..7cb23e54aa1b7 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml @@ -40,6 +40,7 @@ <actionGroup ref="logout" stepKey="logout"/> </after> + <!-- Create completed order --> <actionGroup ref="CreateOrderActionGroup" stepKey="createOrderd"> <argument name="product" value="$$createSimpleProduct$$"/> @@ -63,19 +64,29 @@ <!-- Cancel order --> <actionGroup ref="cancelPendingOrder" stepKey="cancelOrder"/> - <!-- Generate Order report --> + <!-- 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.canceledOrders}}" stepKey="grabCanceledOrdersPrice"/> + <grabTextFrom selector="{{GeneratedReportSection.ordersCount}}" stepKey="grabCanceledOrdersAny"/> <!-- Compare canceled orders price --> - <assertEquals expected="$0.00" expectedType="string" actual="{$grabCanceledOrdersPrice}" actualType="string" stepKey="assertEquals"/> + <assertEquals expected="{$grabCanceledOrdersSpecified}" expectedType="string" actual="{$grabCanceledOrdersAny}" actualType="string" stepKey="assertEquals"/> </test> </tests> diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php index 038d37a990442..cb4d51e0c540d 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php @@ -12,6 +12,7 @@ use Magento\Catalog\Model\Product\Type as ProductType; use Magento\Catalog\Model\ResourceModel\Helper; use Magento\Catalog\Model\ResourceModel\Product as ResourceProduct; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; use Magento\Catalog\Model\ResourceModel\Url; use Magento\Customer\Api\GroupManagementInterface; use Magento\Customer\Model\Session; @@ -25,7 +26,9 @@ use Magento\Framework\Data\Collection\EntityFactory; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; use Magento\Framework\Module\Manager; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; @@ -34,6 +37,7 @@ use Magento\Quote\Model\ResourceModel\Quote\Collection; use Magento\Reports\Model\Event\TypeFactory; use Magento\Reports\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; use Psr\Log\LoggerInterface; @@ -78,46 +82,6 @@ class CollectionTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->objectManager = new ObjectManager($this); - $context = $this->createPartialMock(Context::class, ['getResource', 'getEavConfig']); - $entityFactoryMock = $this->createMock(EntityFactory::class); - $loggerMock = $this->createMock(LoggerInterface::class); - $fetchStrategyMock = $this->createMock(FetchStrategyInterface::class); - $eventManagerMock = $this->createMock(ManagerInterface::class); - $eavConfigMock = $this->createMock(Config::class); - $this->resourceMock = $this->createPartialMock(ResourceConnection::class, ['getTableName', 'getConnection']); - $eavEntityFactoryMock = $this->createMock(EavEntityFactory::class); - $resourceHelperMock = $this->createMock(Helper::class); - $universalFactoryMock = $this->createMock(UniversalFactory::class); - $storeManagerMock = $this->createPartialMockForAbstractClass( - StoreManagerInterface::class, - ['getStore', 'getId'] - ); - $moduleManagerMock = $this->createMock(Manager::class); - $productFlatStateMock = $this->createMock(State::class); - $scopeConfigMock = $this->createMock(ScopeConfigInterface::class); - $optionFactoryMock = $this->createMock(OptionFactory::class); - $catalogUrlMock = $this->createMock(Url::class); - $localeDateMock = $this->createMock(TimezoneInterface::class); - $customerSessionMock = $this->createMock(Session::class); - $dateTimeMock = $this->createMock(DateTime::class); - $groupManagementMock = $this->createMock(GroupManagementInterface::class); - $eavConfig = $this->createPartialMock(Config::class, ['getEntityType']); - $entityType = $this->createMock(Type::class); - - $eavConfig->expects($this->atLeastOnce())->method('getEntityType')->willReturn($entityType); - $context->expects($this->atLeastOnce())->method('getResource')->willReturn($this->resourceMock); - $context->expects($this->atLeastOnce())->method('getEavConfig')->willReturn($eavConfig); - - $defaultAttributes = $this->createPartialMock(DefaultAttributes::class, ['_getDefaultAttributes']); - $productMock = $this->objectManager->getObject( - ResourceProduct::class, - ['context' => $context, 'defaultAttributes' => $defaultAttributes] - ); - - $this->eventTypeFactoryMock = $this->createMock(TypeFactory::class); - $productTypeMock = $this->createMock(ProductType::class); - $quoteResourceMock = $this->createMock(Collection::class); - $this->connectionMock = $this->createPartialMockForAbstractClass(AdapterInterface::class, ['select']); $this->selectMock = $this->createPartialMock( Select::class, [ @@ -130,39 +94,65 @@ protected function setUp() 'having', ] ); - - $storeManagerMock->expects($this->atLeastOnce())->method('getStore')->willReturn($storeManagerMock); - $storeManagerMock->expects($this->atLeastOnce())->method('getId')->willReturn(1); - $universalFactoryMock->expects($this->atLeastOnce())->method('create')->willReturn($productMock); + $this->connectionMock = $this->createMock(AdapterInterface::class); + $this->connectionMock->expects($this->atLeastOnce())->method('select')->willReturn($this->selectMock); + $this->resourceMock = $this->createPartialMock(ResourceConnection::class, ['getTableName', 'getConnection']); $this->resourceMock->expects($this->atLeastOnce())->method('getTableName')->willReturn('test_table'); $this->resourceMock->expects($this->atLeastOnce())->method('getConnection')->willReturn($this->connectionMock); - $this->connectionMock->expects($this->atLeastOnce())->method('select')->willReturn($this->selectMock); + $eavConfig = $this->createPartialMock(Config::class, ['getEntityType']); + $eavConfig->expects($this->atLeastOnce())->method('getEntityType')->willReturn($this->createMock(Type::class)); + $context = $this->createPartialMock(Context::class, ['getResource', 'getEavConfig']); + $context->expects($this->atLeastOnce())->method('getResource')->willReturn($this->resourceMock); + $context->expects($this->atLeastOnce())->method('getEavConfig')->willReturn($eavConfig); + $storeMock = $this->createMock(StoreInterface::class); + $storeMock->expects($this->atLeastOnce())->method('getId')->willReturn(1); + $storeManagerMock = $this->createMock(StoreManagerInterface::class); + $storeManagerMock->expects($this->atLeastOnce())->method('getStore')->willReturn($storeMock); + $productMock = $this->objectManager->getObject( + ResourceProduct::class, + [ + 'context' => $context, + 'defaultAttributes' => $this->createPartialMock( + DefaultAttributes::class, + ['_getDefaultAttributes'] + ) + ] + ); + $resourceModelPoolMock = $this->createMock(ResourceModelPoolInterface::class); + $resourceModelPoolMock->expects($this->atLeastOnce())->method('get')->willReturn($productMock); + $this->eventTypeFactoryMock = $this->createMock(TypeFactory::class); $this->collection = new ProductCollection( - $entityFactoryMock, - $loggerMock, - $fetchStrategyMock, - $eventManagerMock, - $eavConfigMock, + $this->createMock(EntityFactory::class), + $this->createMock(LoggerInterface::class), + $this->createMock(FetchStrategyInterface::class), + $this->createMock(ManagerInterface::class), + $this->createMock(Config::class), $this->resourceMock, - $eavEntityFactoryMock, - $resourceHelperMock, - $universalFactoryMock, + $this->createMock(EavEntityFactory::class), + $this->createMock(Helper::class), + $this->createMock(UniversalFactory::class), $storeManagerMock, - $moduleManagerMock, - $productFlatStateMock, - $scopeConfigMock, - $optionFactoryMock, - $catalogUrlMock, - $localeDateMock, - $customerSessionMock, - $dateTimeMock, - $groupManagementMock, + $this->createMock(Manager::class), + $this->createMock(State::class), + $this->createMock(ScopeConfigInterface::class), + $this->createMock(OptionFactory::class), + $this->createMock(Url::class), + $this->createMock(TimezoneInterface::class), + $this->createMock(Session::class), + $this->createMock(DateTime::class), + $this->createMock(GroupManagementInterface::class), $productMock, $this->eventTypeFactoryMock, - $productTypeMock, - $quoteResourceMock, - $this->connectionMock + $this->createMock(ProductType::class), + $this->createMock(Collection::class), + $this->connectionMock, + $this->createMock(ProductLimitationFactory::class), + $this->createMock(MetadataPool::class), + $this->createMock(\Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer::class), + $this->createMock(\Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver::class), + $this->createMock(\Magento\Framework\Indexer\DimensionFactory::class), + $resourceModelPoolMock ); } @@ -262,25 +252,4 @@ public function testAddViewsCount() $this->collection->addViewsCount(); } - - /** - * Get mock for abstract class with methods. - * - * @param string $className - * @param array $methods - * - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function createPartialMockForAbstractClass($className, $methods) - { - return $this->getMockForAbstractClass( - $className, - [], - '', - true, - true, - true, - $methods - ); - } } 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/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php index 35187e46933bc..6217729f53e50 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,7 +69,7 @@ public function execute() if ($nextId) { $resultRedirect->setPath('review/*/edit', ['id' => $nextId]); } elseif ($this->getRequest()->getParam('ret') == 'pending') { - $resultRedirect->setPath('*/*/pending'); + $resultRedirect->setPath('review/*/pending'); } else { $resultRedirect->setPath('*/*/'); } diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php index 3033a31ff1723..d4e50a9e43d68 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php @@ -5,10 +5,14 @@ */ namespace Magento\Review\Model\ResourceModel\Review\Product; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +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; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; /** * Review Product Collection @@ -88,7 +92,10 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * @param ProductLimitationFactory|null $productLimitationFactory * @param MetadataPool|null $metadataPool - * + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory + * @param ResourceModelPoolInterface|null $resourceModelPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -115,7 +122,11 @@ public function __construct( \Magento\Review\Model\Rating\Option\VoteFactory $voteFactory, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, ProductLimitationFactory $productLimitationFactory = null, - MetadataPool $metadataPool = null + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null, + ResourceModelPoolInterface $resourceModelPool = null ) { $this->_ratingFactory = $ratingFactory; $this->_voteFactory = $voteFactory; @@ -141,7 +152,11 @@ public function __construct( $groupManagement, $connection, $productLimitationFactory, - $metadataPool + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $resourceModelPool ); } 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/etc/acl.xml b/app/code/Magento/Review/etc/acl.xml index 397cc1cce61d6..09b80750da14d 100644 --- a/app/code/Magento/Review/etc/acl.xml +++ b/app/code/Magento/Review/etc/acl.xml @@ -17,7 +17,7 @@ <resource id="Magento_Backend::marketing"> <resource id="Magento_Backend::marketing_user_content"> <resource id="Magento_Review::reviews_all" title="Reviews" translate="title" sortOrder="10"/> - <resource id="Magento_Review::pending" title="Reviews" translate="title" sortOrder="20"/> + <resource id="Magento_Review::pending" title="Pending Reviews" translate="title" sortOrder="20"/> </resource> </resource> </resource> diff --git a/app/code/Magento/Review/etc/adminhtml/menu.xml b/app/code/Magento/Review/etc/adminhtml/menu.xml index e3532483f88af..0a2e49450e0cf 100644 --- a/app/code/Magento/Review/etc/adminhtml/menu.xml +++ b/app/code/Magento/Review/etc/adminhtml/menu.xml @@ -9,6 +9,7 @@ <menu> <add id="Magento_Review::catalog_reviews_ratings_ratings" title="Rating" translate="title" module="Magento_Review" sortOrder="60" parent="Magento_Backend::stores_attributes" action="review/rating/" resource="Magento_Review::ratings"/> <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> + <add id="Magento_Review::catalog_reviews_ratings_pending" title="Pending Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="20" action="review/product/pending" resource="Magento_Review::pending"/> <add id="Magento_Review::report_review" title="Reviews" translate="title" module="Magento_Reports" sortOrder="20" parent="Magento_Reports::report" resource="Magento_Reports::review"/> <add id="Magento_Review::report_review_customer" title="By Customers" translate="title" sortOrder="10" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/customer" resource="Magento_Reports::review_customer"/> <add id="Magento_Review::report_review_product" title="By Products" translate="title" sortOrder="20" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/product" resource="Magento_Reports::review_product"/> diff --git a/app/code/Magento/Robots/Model/Config/Value.php b/app/code/Magento/Robots/Model/Config/Value.php index 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/Model/Condition/AbstractCondition.php b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php index d2be99757df47..6729fe722de56 100644 --- a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php +++ b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php @@ -106,8 +106,8 @@ public function getDefaultOperatorInputByType() 'string' => ['==', '!=', '>=', '>', '<=', '<', '{}', '!{}', '()', '!()'], 'numeric' => ['==', '!=', '>=', '>', '<=', '<', '()', '!()'], 'date' => ['==', '>=', '<='], - 'select' => ['==', '!='], - 'boolean' => ['==', '!='], + 'select' => ['==', '!=', '<=>'], + 'boolean' => ['==', '!=', '<=>'], 'multiselect' => ['{}', '!{}', '()', '!()'], 'grid' => ['()', '!()'], ]; @@ -137,6 +137,7 @@ public function getDefaultOperatorOptions() '!{}' => __('does not contain'), '()' => __('is one of'), '!()' => __('is not one of'), + '<=>' => __('is undefined'), ]; } return $this->_defaultOperatorOptions; 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 8e36562ebd7fe..202337c39da35 100644 --- a/app/code/Magento/Rule/view/adminhtml/web/rules.js +++ b/app/code/Magento/Rule/view/adminhtml/web/rules.js @@ -101,6 +101,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)); } @@ -262,6 +265,8 @@ define([ label.innerHTML = str != '' ? str : '...'; } + this.changeVisibilityForValueRuleParam(elem); + elem = Element.down(container, 'input.input-text'); if (elem) { @@ -293,6 +298,23 @@ define([ this.shownElement = null; }, + changeVisibilityForValueRuleParam: function(elem) { + let parsedElementId = elem.id.split('__'); + if (parsedElementId[2] != 'operator') { + return false; + } + + let 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/Account.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php index bb24d2ae15a34..c2a0eadf7b67a 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 @@ -9,7 +9,6 @@ use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Framework\Pricing\PriceCurrencyInterface; -use Magento\Store\Model\ScopeInterface; /** * Create order account form @@ -133,8 +132,9 @@ protected function _prepareForm() $this->_addAttributesToForm($attributes, $fieldset); $this->_form->addFieldNameSuffix('order[account]'); - $storeId = (int)$this->_sessionQuote->getStoreId(); - $this->_form->setValues($this->extractValuesFromAttributes($attributes, $storeId)); + + $formValues = $this->extractValuesFromAttributes($attributes); + $this->_form->setValues($formValues); return $this; } @@ -192,10 +192,9 @@ public function getFormValues() * Extract the form values from attributes. * * @param array $attributes - * @param int $storeId * @return array */ - private function extractValuesFromAttributes(array $attributes, int $storeId): array + private function extractValuesFromAttributes(array $attributes): array { $formValues = $this->getFormValues(); foreach ($attributes as $code => $attribute) { @@ -203,26 +202,8 @@ private function extractValuesFromAttributes(array $attributes, int $storeId): a if (isset($defaultValue) && !isset($formValues[$code])) { $formValues[$code] = $defaultValue; } - if ($code === 'group_id' && empty($defaultValue)) { - $formValues[$code] = $this->getDefaultCustomerGroup($storeId); - } } return $formValues; } - - /** - * Gets default customer group. - * - * @param int $storeId - * @return string|null - */ - private function getDefaultCustomerGroup(int $storeId): ?string - { - return $this->_scopeConfig->getValue( - 'customer/create_account/default_group', - ScopeInterface::SCOPE_STORE, - $storeId - ); - } } 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 aac625e7c3800..83e66bbbce7cc 100644 --- a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php @@ -270,23 +270,6 @@ public function getTotalAmount($item) return $totalAmount; } - /** - * Return the base total amount minus discount. - * - * @param OrderItem|InvoiceItem|CreditmemoItem $item - * @return mixed - */ - public function getBaseTotalAmount($item) - { - $baseTotalAmount = $item->getBaseRowTotal() - + $item->getBaseTaxAmount() - + $item->getBaseDiscountTaxCompensationAmount() - + $item->getBaseWeeeTaxAppliedAmount() - - $item->getBaseDiscountAmount(); - - return $baseTotalAmount; - } - /** * Return HTML for item total after discount * 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 770fac05a8ca7..48deddb2fe5ac 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -696,7 +696,7 @@ private function canCreditmemoForZeroTotalRefunded($totalRefunded) * @param float $totalRefunded * @return bool */ - public function canCreditmemoForZeroTotal($totalRefunded) + private function canCreditmemoForZeroTotal($totalRefunded) { $totalPaid = $this->getTotalPaid(); //check if total paid is less than grandtotal @@ -1311,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); diff --git a/app/code/Magento/Sales/Model/Order/Address/Renderer.php b/app/code/Magento/Sales/Model/Order/Address/Renderer.php index ba0f2f852e727..947c92e04942f 100644 --- a/app/code/Magento/Sales/Model/Order/Address/Renderer.php +++ b/app/code/Magento/Sales/Model/Order/Address/Renderer.php @@ -9,8 +9,6 @@ use Magento\Customer\Model\Address\Config as AddressConfig; use Magento\Framework\Event\ManagerInterface as EventManager; use Magento\Sales\Model\Order\Address; -use Magento\Framework\Stdlib\StringUtils; -use Magento\Framework\App\ObjectManager; /** * Class Renderer used for formatting an order address @@ -29,26 +27,18 @@ class Renderer */ protected $eventManager; - /** - * @var StringUtils - */ - private $stringUtils; - /** * Constructor * * @param AddressConfig $addressConfig * @param EventManager $eventManager - * @param StringUtils $stringUtils */ public function __construct( AddressConfig $addressConfig, - EventManager $eventManager, - StringUtils $stringUtils = null + EventManager $eventManager ) { $this->addressConfig = $addressConfig; $this->eventManager = $eventManager; - $this->stringUtils = $stringUtils ?: ObjectManager::getInstance()->get(StringUtils::class); } /** @@ -68,50 +58,4 @@ public function format(Address $address, $type) $this->eventManager->dispatch('customer_address_format', ['type' => $formatType, 'address' => $address]); return $formatType->getRenderer()->renderArray($address->getData()); } - - /** - * Detect an input string is Arabic - * - * @param string $subject - * @return bool - */ - public function isArabic(string $subject): bool - { - return (preg_match('/\p{Arabic}/u', $subject) > 0); - } - - /** - * Reverse text with Arabic characters - * - * @param string $string - * @return string - */ - public 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->isArabic($splitText[$j])) - ? $this->stringUtils->strrev($splitText[$j]) : $splitText[$j]; - $splitText[$j] = ($this->isArabic($splitText[$i])) - ? $this->stringUtils->strrev($splitText[$i]) : $splitText[$i]; - $splitText[$i] = $tmp; - } - } - } - return implode(' ', $splitText); - } - - /** - * Check and revert arabic text - * - * @param string $string - * @return string - */ - public function processArabicText($string) - { - return ($this->isArabic($string)) - ? $this->reverseArabicText($string) : $string; - } } 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/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/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/Pdf/AbstractPdf.php b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php index 5830d236b1a6d..85e34f560bb7b 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php @@ -474,8 +474,7 @@ protected function insertOrder(&$page, $obj, $putOrderId = true) if ($value !== '') { $text = []; foreach ($this->string->split($value, 45, true, true) as $_value) { - $text[] = ($this->addressRenderer->isArabic($_value)) - ? $this->addressRenderer->reverseArabicText($_value) : $_value; + $text[] = $_value; } foreach ($text as $part) { $page->drawText(strip_tags(ltrim($part)), 35, $this->y, 'UTF-8'); @@ -492,8 +491,7 @@ protected function insertOrder(&$page, $obj, $putOrderId = true) if ($value !== '') { $text = []; foreach ($this->string->split($value, 45, true, true) as $_value) { - $text[] = ($this->addressRenderer->isArabic($_value)) - ? $this->addressRenderer->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/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php index 2b57bea85a1bb..8562328025540 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 @@ -5,9 +5,6 @@ */ namespace Magento\Sales\Model\Order\Pdf\Items\Invoice; -use Magento\Framework\App\ObjectManager; -use Magento\Sales\Model\Order\Address\Renderer; - /** * Sales Order Invoice Pdf default items renderer */ @@ -20,11 +17,6 @@ class DefaultInvoice extends \Magento\Sales\Model\Order\Pdf\Items\AbstractItems */ protected $string; - /** - * @var \Magento\Sales\Model\Order\Address\Renderer - */ - private $renderer; - /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -35,8 +27,6 @@ class DefaultInvoice extends \Magento\Sales\Model\Order\Pdf\Items\AbstractItems * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data - * @param \Magento\Sales\Model\Order\Address\Renderer $renderer - * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -47,8 +37,7 @@ public function __construct( \Magento\Framework\Stdlib\StringUtils $string, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [], - Renderer $renderer = null + array $data = [] ) { $this->string = $string; parent::__construct( @@ -61,7 +50,6 @@ public function __construct( $resourceCollection, $data ); - $this->renderer = $renderer ?: ObjectManager::getInstance()->get(Renderer::class); } /** @@ -78,16 +66,11 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [ - [ - 'text' => $this->string->split($this->renderer->processArabicText($item->getName()), 35, true, true), - 'feed' => 35 - ] - ]; + $lines[0] = [['text' => $this->string->split($item->getName(), 35, true, true), 'feed' => 35]]; // draw SKU $lines[0][] = [ - 'text' => $this->string->split($this->renderer->processArabicText($item->getSku($item)), 17), + 'text' => $this->string->split($this->getSku($item), 17), 'feed' => 290, 'align' => 'right', ]; diff --git a/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php b/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php index a1015c102b3af..38728d88ff4fa 100644 --- a/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php +++ b/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php @@ -52,7 +52,11 @@ public function execute( $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] = $this->defaultRenderer->getBaseTotalAmount($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/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/Service/InvoiceService.php b/app/code/Magento/Sales/Model/Service/InvoiceService.php index 791274d3f23bd..02242e92c8bf5 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); } /** @@ -172,13 +187,18 @@ private function prepareItemsQty(Order $order, array $qtys = []) { foreach ($order->getAllItems() as $orderItem) { if (empty($qtys[$orderItem->getId()])) { - $parentId = $orderItem->getParentItemId(); - if ($parentId && array_key_exists($parentId, $qtys)) { - $qtys[$orderItem->getId()] = $qtys[$parentId]; + 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; } } + $this->prepareItemQty($orderItem, $qtys); } @@ -186,20 +206,27 @@ private function prepareItemsQty(Order $order, array $qtys = []) } /** - * Prepare qty to invoice item. + * Prepare qty_invoiced for order item * - * @param Order\Item $orderItem + * @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem * @param array $qtys - * @return void */ 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(); } + $parentId = $orderItem->getParentItemId(); + if ($parentId && array_key_exists($parentId, $qtys)) { + $qtys[$orderItem->getId()] = $qtys[$parentId]; + } else { + continue; + } } } elseif ($orderItem->getParentItem()) { $parent = $orderItem->getParentItem(); @@ -210,6 +237,26 @@ private function prepareItemQty(\Magento\Sales\Api\Data\OrderItemInterface $orde } } + /** + * 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']; + } + } + } + /** * Check if order item can be invoiced. * diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml index 820de20c56d54..abc5698cc71e6 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml @@ -17,23 +17,23 @@ <waitForPageLoad stepKey="waitForCardSelected"/> <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"/> <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"/> <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> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/OrderAndReturnActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml similarity index 64% rename from app/code/Magento/Sales/Test/Mftf/ActionGroup/OrderAndReturnActionGroup.xml rename to app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml index c46dd612022fd..fcea25f997591 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/OrderAndReturnActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml @@ -9,11 +9,11 @@ <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"> + <actionGroup name="StorefrontSearchGuestOrderActionGroup"> <arguments> <argument name="orderId" type="string"/> - <argument name="orderLastName"/> - <argument name="orderEmail"/> + <argument name="orderLastName" type="string"/> + <argument name="orderEmail" type="string"/> </arguments> <amOnPage url="{{StorefrontOrdersAndReturnsPage.url}}" stepKey="navigateToOrderAndReturnPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> @@ -24,13 +24,4 @@ <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/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/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/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/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/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/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/AdminCorrectnessInvoicedItemInBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml new file mode 100644 index 0000000000000..7c83f35468ce6 --- /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="submitInvoiceIntoOrder" 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/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/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/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/OrderSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php index 46c44c03b1514..88053ea684ce8 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php @@ -64,7 +64,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen $this->orderMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -72,7 +72,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen ->willReturn($configValue); if (!$configValue || $forceSyncMode) { - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -118,7 +118,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen $this->orderMock->expects($this->once()) ->method('setEmailSent') - ->with(true); + ->with($emailSendingResult); $this->orderResourceMock->expects($this->once()) ->method('saveAttribute') @@ -210,7 +210,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ->with('sales_email/general/async_sending') ->willReturn(false); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(true); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php index 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/OrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php index 61ae9ae45e12f..705d2face2308 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php @@ -428,22 +428,6 @@ public function testCanCreditMemo() $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->prepareOrderItem(); - $this->order->setGrandTotal($grandTotal); - $this->order->setTotalPaid($totalPaid); - $this->assertFalse($this->order->canCreditmemoForZeroTotal($totalRefunded)); - } - public function testCanNotCreditMemoWithTotalNull() { $totalPaid = 0; 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/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/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/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/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/reorder/sidebar.phtml b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml index 9b3633fde60b4..a2ab3d02b13ea 100644 --- a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml @@ -57,7 +57,7 @@ </button> </div> <div class="secondary"> - <a class="action view" href="<?= /* @escapeNotVerified */ $block->getUrl('customer/account') ?>"> + <a class="action view" href="<?= /* @escapeNotVerified */ $block->getUrl('customer/account') ?>#my-orders-table"> <span><?= /* @escapeNotVerified */ __('View All') ?></span> </a> </div> diff --git a/app/code/Magento/SalesRule/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/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/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 048dea3f32547..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. @@ -89,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/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index 2e841f3dc5fa5..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> @@ -179,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 7628ecf468827..c8da82407457d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -48,9 +48,12 @@ <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"/> 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/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/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/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/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/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml new file mode 100644 index 0000000000000..9e5bde9a2be49 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.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="StorefrontQuickSearchResultsSection"> + <element name="searchTextBox" type="text" selector="#search"/> + <element name="searchTextBoxButton" type="button" selector="button[class='action search']"/> + <element name="productLink" type="select" selector="a[class='product-item-link']"/> + <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/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/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index 0dadb0e243864..27047ae46bf1f 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -29,14 +29,6 @@ abstract class AbstractCarrierOnline extends AbstractCarrier const GUAM_REGION_CODE = 'GU'; - const SPAIN_COUNTRY_ID = 'ES'; - - const CANARY_ISLANDS_COUNTRY_ID = 'IC'; - - const SANTA_CRUZ_DE_TENERIFE_REGION_ID = 'Santa Cruz de Tenerife'; - - const LAS_PALMAS_REGION_ID = 'Las Palmas'; - /** * Array of quotes * 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/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/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..bd7a84f601b77 100644 --- a/app/code/Magento/Sitemap/Model/Observer.php +++ b/app/code/Magento/Sitemap/Model/Observer.php @@ -5,6 +5,12 @@ */ namespace Magento\Sitemap\Model; +use Magento\Store\Model\App\Emulation; +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 +50,40 @@ 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; + private $collectionFactory; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var Emulation */ - protected $_storeManager; + private $appEmulation; /** - * @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 + * @param Emulation $appEmulation */ 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, + Emulation $appEmulation ) { - $this->_scopeConfig = $scopeConfig; - $this->_collectionFactory = $collectionFactory; - $this->_storeManager = $storeManager; - $this->_transportBuilder = $transportBuilder; - $this->inlineTranslation = $inlineTranslation; + $this->scopeConfig = $scopeConfig; + $this->collectionFactory = $collectionFactory; + $this->appEmulation = $appEmulation; + $this->emailNotification = $emailNotification; } /** @@ -97,61 +96,39 @@ 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 */ try { + $this->appEmulation->startEnvironmentEmulation( + $sitemap->getStoreId(), + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ); + $sitemap->generateXml(); } catch (\Exception $e) { $errors[] = $e->getMessage(); + } finally { + $this->appEmulation->stopEnvironmentEmulation(); } } - - 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/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/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..5fae54ff3c5d0 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->once()) + $this->sitemapMock->expects($this->at(0)) + ->method('getStoreId') + ->willReturn($storeId); + + $this->sitemapMock->expects($this->at(1)) ->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,43 +130,16 @@ 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->appEmulationMock->expects($this->at(0)) + ->method('startEnvironmentEmulation') + ->with( + $storeId, + Area::AREA_FRONTEND, + true + ); - $this->inlineTranslationMock->expects($this->once()) - ->method('resume'); + $this->appEmulationMock->expects($this->at(1)) + ->method('stopEnvironmentEmulation'); $this->observer->scheduledGenerateSitemaps(); } diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml index 91fe4fccddb91..7f1a63d3db6f2 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml @@ -25,4 +25,21 @@ <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> </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..9b942109785d4 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml @@ -15,6 +15,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,8 +23,9 @@ <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" /> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarningAboutTakingALongTimeToComplete" /> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmModal" /> + <waitForPageLoad stepKey="waitForStorePageLoad" /> <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> </actionGroup> <!--Save the Store view--> @@ -37,4 +39,27 @@ <waitForLoadingMaskToDisappear stepKey="waitForForm"/> <see userInput="Store with the same code already exists." stepKey="seeMessage" /> </actionGroup> + <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/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/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/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/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/AdminStoresGridSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml index 1b84027a5dd4a..592af42f2de30 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml @@ -14,12 +14,14 @@ <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="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']"/> </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 af24a3b9d29f8..416808e58e6b3 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -9,8 +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/AdminCreateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml index e7a3d03f337db..af8aceed07f0f 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml @@ -33,6 +33,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2" /> <actionGroup ref="AdminCreateStoreViewCodeUniquenessActionGroup" stepKey="createStoreViewCode" /> <after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> <actionGroup ref="logout" stepKey="logout"/> </after> </test> 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/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..e40a04080285a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -14,5 +14,6 @@ <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/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/view/adminhtml/web/css/swatches.css b/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css index ef635c48e3466..b0ea10b1ed968 100644 --- a/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css +++ b/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css @@ -153,6 +153,18 @@ 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; } 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 962ac8b171692..2080652210a53 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 @@ -493,7 +493,7 @@ define([ return ''; } - $.each(config.options, function () { + $.each(config.options, function (index) { var id, type, value, @@ -511,7 +511,7 @@ 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; @@ -523,6 +523,7 @@ define([ label = this.label ? this.label : ''; attr = ' id="' + controlId + '-item-' + id + '"' + + ' index="' + index + '"' + ' aria-checked="false"' + ' aria-describedby="' + controlId + '"' + ' tabindex="0"' + @@ -745,6 +746,12 @@ define([ $widget._UpdatePrice(); } + $(document).trigger('updateMsrpPriceBlock', + [ + parseInt($this.attr('index'), 10) + 1, + $widget.options.jsonConfig.optionPrices + ]); + $widget._loadMedia(); $input.trigger('change'); }, @@ -917,7 +924,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 +942,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) { @@ -1229,7 +1235,10 @@ define([ } imagesToUpdate = this._setImageIndex(imagesToUpdate); - gallery.updateData(imagesToUpdate); + + if (!_.isUndefined(gallery)) { + gallery.updateData(imagesToUpdate); + } if (isInitial) { $(this.options.mediaGallerySelector).AddFotoramaVideoEvents(); 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..bff489ee50c2f 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, @@ -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,7 +471,7 @@ 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) @@ -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 */ @@ -846,8 +904,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/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/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/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/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/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/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/Test/Mftf/Section/AdminDesignConfigSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml index 0aa2f7f35218a..e90548a7c94e9 100644 --- a/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml +++ b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml @@ -14,5 +14,10 @@ <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="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']"/> </section> </sections> 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..f719f5dd78307 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 @@ -14,8 +14,8 @@ <span data-action="toggle-nav" class="action nav-toggle"><span><?= /* @escapeNotVerified */ __('Toggle Nav') ?></span></span> <a class="logo" href="<?= $block->getUrl('') ?>" title="<?= /* @escapeNotVerified */ $storeName ?>"> <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/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/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/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/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/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/i18n/en_US.csv b/app/code/Magento/Ui/i18n/en_US.csv index d51ff98108376..039e28f318176 100644 --- a/app/code/Magento/Ui/i18n/en_US.csv +++ b/app/code/Magento/Ui/i18n/en_US.csv @@ -190,4 +190,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/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/web/js/dynamic-rows/record.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js index 54309ca068513..3987507ece54f 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'); }); 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/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/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/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/search/search.js b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js index 19536e7ff8c18..999e3262dbbdd 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js @@ -18,7 +18,7 @@ define([ return Element.extend({ defaults: { template: 'ui/grid/search/search', - placeholder: $t('Search by keyword'), + placeholder: 'Search by keyword', label: $t('Keyword'), value: '', previews: [], diff --git a/app/code/Magento/Ui/view/base/web/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/validation/validator.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js index be8fd2ce9fcef..8ebbf88775b86 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); 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/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 82205de4156ad..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 @@ -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..13b82a93eca25 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html @@ -10,9 +10,10 @@ </label> <input class="admin__control-text data-grid-search-control" type="text" data-bind=" + i18n: placeholder, attr: { id: index, - placeholder: placeholder + placeholder: $t(placeholder) }, textInput: inputValue, keyboard: { diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html b/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html index c5d87a4b16c4e..610d78e00b81d 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html @@ -6,7 +6,7 @@ --> <ul class="action-submenu" each="data: action.actions, as: 'action'" css="_active: action.visible"> <li css="_visible: $data.visible"> - <span class="action-menu-item" text="label" click="$parent.applyAction.bind($parent, type)"/> + <span class="action-menu-item" translate="label" click="$parent.applyAction.bind($parent, type)"/> <render args="name: $parent.submenuTemplate, data: $parent" if="$data.actions"/> </li> </ul> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html b/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html index 1aeb48b7c7698..d11d4aa243737 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html @@ -11,7 +11,7 @@ <div class="action-menu-items"> <ul class="action-menu" each="data: actions, as: 'action'" css="_active: opened"> <li css="_visible: $data.visible, _parent: $data.actions"> - <span class="action-menu-item" text="label" click="$parent.applyAction.bind($parent, type)"/> + <span class="action-menu-item" translate="label" click="$parent.applyAction.bind($parent, type)"/> <render args="name: $parent.submenuTemplate, data: $parent" if="$data.actions"/> </li> </ul> diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index 43efb6a91c01f..8c60f5a53a2d9 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -333,11 +333,11 @@ public function setRequest(RateRequest $request) } // For UPS, Las Palmas and Santa Cruz de Tenerife will be represented by Canary Islands country - if ($destCountry === self::SPAIN_COUNTRY_ID && - ($request->getDestRegionCode() === self::LAS_PALMAS_REGION_ID - || $request->getDestRegionCode() === self::SANTA_CRUZ_DE_TENERIFE_REGION_ID) + if ($destCountry === 'ES' && + ($request->getDestRegionCode() === 'Las Palmas' + || $request->getDestRegionCode() === 'Santa Cruz de Tenerife') ) { - $destCountry = self::CANARY_ISLANDS_COUNTRY_ID; + $destCountry = 'IC'; } $country = $this->_countryFactory->create()->load($destCountry); diff --git a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php index 34e82f561ee80..e1bb094e7fc39 100644 --- a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php +++ b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php @@ -68,25 +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::REQUEST_PATH => $urlPath, + UrlRewrite::TARGET_PATH => $oldRewrite->getTargetPath(), UrlRewrite::STORE_ID => $targetStore->getId(), ]); - if (null === $currentRewrite) { - /** @var \Magento\Framework\App\Response\Http $response */ - $targetUrl = $targetStore->getBaseUrl(); + if ($currentRewrite) { + $targetUrl .= $currentRewrite->getRequestPath(); } } else { $existingRewrite = $this->urlFinder->findOneByData([ UrlRewrite::REQUEST_PATH => $urlPath ]); - if ($existingRewrite) { + $currentRewrite = $this->urlFinder->findOneByData([ + UrlRewrite::REQUEST_PATH => $urlPath, + UrlRewrite::STORE_ID => $targetStore->getId(), + ]); + + 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..e9651a3f26e94 --- /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/UrlRewriteData.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml new file mode 100644 index 0000000000000..77cf80ca95ac5 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml @@ -0,0 +1,46 @@ +<?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> + </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/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 731276518036c..7b789845fe249 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml @@ -13,7 +13,14 @@ <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="requestPathColumn" type="text" selector="//tr[@data-role='row']['{{var1}}']/td[@data-column='request_path']" parameterized="true"/> + <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/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest.xml new file mode 100644 index 0000000000000..ee3682c8ad4d7 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest.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="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="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/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..7453b7b5a43f3 --- /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, no 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/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/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/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..696dbf166fc38 --- /dev/null +++ b/app/code/Magento/VaultGraphQl/Model/Resolver/DeletePaymentToken.php @@ -0,0 +1,82 @@ +<?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\CheckCustomerAccount; + +/** + * Delete Payment Token resolver, used for GraphQL mutation processing. + */ +class DeletePaymentToken implements ResolverInterface +{ + /** + * @var CheckCustomerAccount + */ + private $checkCustomerAccount; + + /** + * @var PaymentTokenManagementInterface + */ + private $paymentTokenManagement; + + /** + * @var PaymentTokenRepositoryInterface + */ + private $paymentTokenRepository; + + /** + * @param CheckCustomerAccount $checkCustomerAccount + * @param PaymentTokenManagementInterface $paymentTokenManagement + * @param PaymentTokenRepositoryInterface $paymentTokenRepository + */ + public function __construct( + CheckCustomerAccount $checkCustomerAccount, + PaymentTokenManagementInterface $paymentTokenManagement, + PaymentTokenRepositoryInterface $paymentTokenRepository + ) { + $this->checkCustomerAccount = $checkCustomerAccount; + $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.')); + } + + $currentUserId = $context->getUserId(); + $currentUserType = $context->getUserType(); + + $this->checkCustomerAccount->execute($currentUserId, $currentUserType); + + $token = $this->paymentTokenManagement->getByPublicHash($args['public_hash'], $currentUserId); + 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..80e81037bb5dd --- /dev/null +++ b/app/code/Magento/VaultGraphQl/Model/Resolver/PaymentTokens.php @@ -0,0 +1,72 @@ +<?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\CheckCustomerAccount; + +/** + * Customers Payment Tokens resolver, used for GraphQL request processing. + */ +class PaymentTokens implements ResolverInterface +{ + /** + * @var PaymentTokenManagement + */ + private $paymentTokenManagement; + + /** + * @var CheckCustomerAccount + */ + private $checkCustomerAccount; + + /** + * @param PaymentTokenManagement $paymentTokenManagement + * @param CheckCustomerAccount $checkCustomerAccount + */ + public function __construct( + PaymentTokenManagement $paymentTokenManagement, + CheckCustomerAccount $checkCustomerAccount + ) { + $this->paymentTokenManagement = $paymentTokenManagement; + $this->checkCustomerAccount = $checkCustomerAccount; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $currentUserId = $context->getUserId(); + $currentUserType = $context->getUserType(); + + $this->checkCustomerAccount->execute($currentUserId, $currentUserType); + + $tokens = $this->paymentTokenManagement->getVisibleAvailableTokens($currentUserId); + $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/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/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..52dc8e7837a3c 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); 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/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index 003b398d5650e..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"/> 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/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/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/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/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 index f011a8bead4f2..4e6a062c7993d 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml @@ -59,7 +59,14 @@ <!--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="This is a required field" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsError}}" stepKey="seeError"/> + <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/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/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/view/frontend/templates/item/column/cart.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml index 848c6a76393f8..9ea0d1a823235 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,6 @@ /** @var \Magento\Wishlist\Model\Item $item */ $item = $block->getItem(); $product = $item->getProduct(); -$allowedQty = $block->getMinMaxQty(); ?> <?php foreach ($block->getChildNames() as $childName): ?> <?= /* @noEscape */ $block->getLayout()->renderElement($childName, false) ?> @@ -22,7 +21,7 @@ $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'] ?>}}" + <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}" name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ (int)($block->getAddToCartQty($item) * 1) ?>"> </div> </div> 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 7a166b47256cb..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(); }, @@ -181,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) { - - element = $('input[type=file]' + self.options.customOptionsInfo); - params = $(event.currentTarget).data('post'); - form = $(element).closest('form'); - action = params.action; + event.stopPropagation(); + event.preventDefault(); - if (params.data.id) { - $('<input>', { - type: 'hidden', - name: 'id', - value: params.data.id - }).appendTo(form); - } + element = $('input[type=file]' + self.options.customOptionsInfo); + params = $(event.currentTarget).data('post'); + form = $(element).closest('form'); + action = params.action; - 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/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/main/_collapsible-blocks.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less index ae25b1102e581..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 @@ -495,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_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/_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/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/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 03caa1c543bba..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 @@ -111,7 +111,7 @@ content: @alert-icon__error__content; font-size: @alert-icon__error__font-size; left: 2.2rem; - margin-top: .5rem; + margin-top: -1.1rem; } } 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/_fields.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less index f47ddcece1c79..d8fd1db5ced47 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 @@ -122,7 +122,7 @@ > .admin__field-control { #mix-grid .column(@field-control-grid__column, @field-grid__columns); input[type="checkbox"] { - margin-top: 1.2rem; + margin-top: @indent__s; } } @@ -542,10 +542,10 @@ & > .admin__field { &:first-child { position: static; - & > .admin__field-label { #mix-grid .column(@field-label-grid__column, @field-grid__columns); cursor: pointer; + background: @color-white; left: 0; position: absolute; top: 0; @@ -660,10 +660,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; @@ -696,6 +697,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 bb0e4a573e643..5d9bf80ce2255 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 @@ -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; } } } @@ -552,7 +552,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/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/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/_tooltip.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_tooltip.less index 39b9a051e6592..664726ddfd798 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 @@ -65,6 +65,10 @@ @_icon-font-color-active: false ); + &:before { + padding-left : 1px; + } + &:focus { ._keyfocus & { .lib-css(z-index, @checkout-tooltip__hover__z-index); 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 4d9cb41b3adac..8cf5cd313edc5 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 @@ -421,7 +421,7 @@ > .field { > .control { - width: 55%; + width: 80%; } } } @@ -451,7 +451,8 @@ .form.password.reset, .form.send.confirmation, .form.password.forget, - .form.create.account { + .form.create.account, + .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_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/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/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less index f15509ceb63eb..d5f90d3e6d546 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); @@ -534,6 +540,15 @@ } } + .block-compare { + .action { + &.delete { + &:extend(.abs-remove-button-for-blocks all); + right: initial; + } + } + } + .action.tocart { border-radius: 0; } diff --git a/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less index f785dd74d900e..0f91f857a715c 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; 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 6be6010fd2d2d..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 @@ -527,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 { 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 fd418e0c447b8..7265d7bd61c51 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 { 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/_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_Customer/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less index 749a388ac2354..5b0f717ff15bc 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; @@ -375,7 +376,7 @@ .fieldset { > .field { > .control { - width: 55%; + width: 80%; } } } @@ -406,7 +407,8 @@ .form.password.reset, .form.send.confirmation, .form.password.forget, - .form.create.account { + .form.create.account, + .form.form-orders-search { min-width: 600px; width: 50%; } diff --git a/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less index 088372808aa6a..fe49d6679a613 100644 --- a/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less @@ -133,9 +133,18 @@ } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { - .table-wrapper.grouped { - .lib-css(margin-left, -@layout__width-xs-indent); - .lib-css(margin-right, -@layout__width-xs-indent); + .product-add-form { + .table-wrapper.grouped { + .lib-css(margin-left, -@layout__width-xs-indent); + .lib-css(margin-right, -@layout__width-xs-indent); + .table.data.grouped { + tr { + td { + padding: 5px 10px 5px 15px; + } + } + } + } } } diff --git a/app/design/frontend/Magento/luma/Magento_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_Multishipping/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less index 38f7c4ca873fd..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; + } + } + } + } + } + } } } 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 4324e5f1d6a93..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 @@ -363,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/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/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less index 1e4a92fa0701f..f2b9c9274bbcf 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; } } } @@ -555,13 +561,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 +693,19 @@ } } } + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__l) { + .order-links { + .item { + margin: 0 @tab-control__margin-right 0 0; + + a { + padding: @tab-control__padding-top @tab-control__padding-right; + } + + strong { + padding: @tab-control__padding-top @tab-control__padding-right @tab-control__padding-bottom + 1 @tab-control__padding-left; + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less index baf5468b18485..3435736a54a6a 100644 --- a/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less @@ -10,6 +10,14 @@ & when (@media-common = true) { .form.send.friend { &:extend(.abs-add-fields all); + + .fieldset { + .field { + .control { + width: 100%; + } + } + } } .product-social-links .action.mailto.friend { @@ -44,3 +52,18 @@ } } } +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .form.send.friend { + .fieldset { + padding-bottom: @indent__xs; + } + + .action { + &.remove { + margin-left: 0; + right: 0; + top: 100%; + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less index 32bb22a632177..4d990a82cb7e4 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 // --------------------------------------------- @@ -458,7 +466,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 3658902feb3a6..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 @@ -182,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 + ); + } + } + } + } } // @@ -229,15 +253,7 @@ &:last-child { margin-right: 0; } - - &.edit { - float: left; - } - - &.delete { - float: right; - } - + &.edit, &.delete { margin-top: 7px; @@ -379,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/etc/di.xml b/app/etc/di.xml index 6cf169c1d2277..19543375aad58 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -153,6 +153,7 @@ <preference for="Magento\Framework\Pricing\Amount\AmountInterface" type="Magento\Framework\Pricing\Amount\Base" /> <preference for="Magento\Framework\Api\SearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> <preference for="Magento\Framework\Api\AttributeInterface" type="Magento\Framework\Api\AttributeValue" /> + <preference for="Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface" type="Magento\Framework\Model\ResourceModel\ResourceModelPool" /> <preference for="Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface" type="Magento\Framework\Model\ResourceModel\Db\TransactionManager" /> <preference for="Magento\Framework\Api\Data\ImageContentInterface" type="Magento\Framework\Api\ImageContent" /> <preference for="Magento\Framework\Api\ImageContentValidatorInterface" type="Magento\Framework\Api\ImageContentValidator" /> diff --git a/auth.json.sample b/auth.json.sample index 48b13ee8b69da..be1c70cfe1e18 100644 --- a/auth.json.sample +++ b/auth.json.sample @@ -1,7 +1,4 @@ { - "github-oauth": { - "github.com": "<github-personal-access-token>" - }, "http-basic": { "repo.magento.com": { "username": "<public-key>", diff --git a/composer.json b/composer.json index 3222de2d1fe0f..50b22238e0d01 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", @@ -84,7 +84,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "~2.13.0", "lusitanian/oauth": "~0.8.10", - "magento/magento2-functional-testing-framework": "~2.3.13", + "magento/magento2-functional-testing-framework": "~2.3.14", "pdepend/pdepend": "2.5.2", "phpmd/phpmd": "@stable", "phpunit/phpunit": "~6.5.0", @@ -104,6 +104,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 +143,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 +181,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": "*", @@ -234,6 +239,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 697e7df3e19aa..a7131f4a16eec 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": "3f58ddc5609e6a934ee3706006357646", + "content-hash": "60007664938710edf52eadddd7551867", "packages": [ { "name": "braintree/braintree_php", @@ -201,16 +201,16 @@ }, { "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,7 +333,7 @@ "dependency", "package" ], - "time": "2018-12-03T09:31:16+00:00" + "time": "2019-02-11T09:52:10+00:00" }, { "name": "composer/semver", @@ -460,16 +460,16 @@ }, { "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 +500,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 +535,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 +588,7 @@ "elasticsearch", "search" ], - "time": "2017-11-08T17:04:47+00:00" + "time": "2019-01-08T18:53:46+00:00" }, { "name": "guzzlehttp/ringphp", @@ -691,23 +693,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 +755,7 @@ "json", "schema" ], - "time": "2018-02-14T22:26:30+00:00" + "time": "2019-01-14T23:55:14+00:00" }, { "name": "magento/composer", @@ -1104,16 +1106,16 @@ }, { "name": "paragonie/sodium_compat", - "version": "v1.8.0", + "version": "v1.8.1", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "5e280b50cdaf8da4cc4810e0847a9618be29703d" + "reference": "57bb5ef079d3724148da3d5c99e30695ab17afda" }, "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/57bb5ef079d3724148da3d5c99e30695ab17afda", + "reference": "57bb5ef079d3724148da3d5c99e30695ab17afda", "shasum": "" }, "require": { @@ -1182,7 +1184,7 @@ "secret-key cryptography", "side-channel resistant" ], - "time": "2018-11-29T22:33:39+00:00" + "time": "2019-01-03T21:00:55+00:00" }, { "name": "pelago/emogrifier", @@ -1380,16 +1382,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.12", + "version": "2.0.14", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "8814dc7841db159daed0b32c2b08fb7e03c6afe7" + "reference": "8ebfcadbf30524aeb75b2c446bc2519d5b321478" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/8814dc7841db159daed0b32c2b08fb7e03c6afe7", - "reference": "8814dc7841db159daed0b32c2b08fb7e03c6afe7", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/8ebfcadbf30524aeb75b2c446bc2519d5b321478", + "reference": "8ebfcadbf30524aeb75b2c446bc2519d5b321478", "shasum": "" }, "require": { @@ -1468,7 +1470,7 @@ "x.509", "x509" ], - "time": "2018-11-04T05:45:48+00:00" + "time": "2019-01-27T19:37:29+00:00" }, { "name": "psr/container", @@ -1700,16 +1702,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 +1744,7 @@ "promise", "promises" ], - "time": "2018-06-13T15:59:06+00:00" + "time": "2019-01-07T21:25:54+00:00" }, { "name": "seld/jsonlint", @@ -1839,16 +1841,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 +1861,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 +1873,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 +1908,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 +1961,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 +2024,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 +2074,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,7 +2123,7 @@ ], "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", @@ -2165,7 +2170,7 @@ }, { "name": "Gert de Pagter", - "email": "backendtea@gmail.com" + "email": "BackEndTea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -2239,16 +2244,16 @@ }, { "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 +2289,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 +2753,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 +2773,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 +2807,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 +3075,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 +3136,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 +3218,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 +3269,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 +3399,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 +3452,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 +4377,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 +4401,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 +4420,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 +4493,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 +4739,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 +4756,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 +4794,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 +4885,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 +4961,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.11.2", "source": { "type": "git", "url": "https://github.com/consolidation/annotated-command.git", - "reference": "288593672a8ca9ead2c73a8bfbfa4737862bfd6a" + "reference": "004af26391cd7d1cd04b0ac736dc1324d1b4f572" }, "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/004af26391cd7d1cd04b0ac736dc1324d1b4f572", + "reference": "004af26391cd7d1cd04b0ac736dc1324d1b4f572", "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 +5057,20 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2018-12-14T01:52:35+00:00" + "time": "2019-02-02T02:29:53+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 +5079,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 +5091,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 +5138,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,7 +5228,7 @@ } ], "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", @@ -5171,20 +5288,20 @@ }, { "name": "consolidation/robo", - "version": "1.3.3", + "version": "1.4.6", "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "01789e1c4f3b0d7e5b4ee0aca1843a9438ff4983" + "reference": "d4805a1abbc730e9a6d64ede2eba56f91a2b4eb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/01789e1c4f3b0d7e5b4ee0aca1843a9438ff4983", - "reference": "01789e1c4f3b0d7e5b4ee0aca1843a9438ff4983", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/d4805a1abbc730e9a6d64ede2eba56f91a2b4eb3", + "reference": "d4805a1abbc730e9a6d64ede2eba56f91a2b4eb3", "shasum": "" }, "require": { - "consolidation/annotated-command": "^2.10.1", + "consolidation/annotated-command": "^2.10.2", "consolidation/config": "^1.0.10", "consolidation/log": "~1", "consolidation/output-formatters": "^3.1.13", @@ -5211,7 +5328,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 +5373,7 @@ } }, "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -5275,7 +5392,7 @@ } ], "description": "Modern task runner", - "time": "2018-12-14T02:54:30+00:00" + "time": "2019-02-17T05:32:27+00:00" }, { "name": "consolidation/self-update", @@ -5778,16 +5895,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 +5931,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 +5982,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", @@ -6503,16 +6620,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "2.3.13", + "version": "2.3.14", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "2c8a4c3557c9a8412eb2ea50ce3f69abc2f47ba1" + "reference": "b4002b3fe53884895921b44cf519d42918e3c7c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/2c8a4c3557c9a8412eb2ea50ce3f69abc2f47ba1", - "reference": "2c8a4c3557c9a8412eb2ea50ce3f69abc2f47ba1", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/b4002b3fe53884895921b44cf519d42918e3c7c6", + "reference": "b4002b3fe53884895921b44cf519d42918e3c7c6", "shasum": "" }, "require": { @@ -6572,7 +6689,7 @@ "magento", "testing" ], - "time": "2019-01-29T15:31:14+00:00" + "time": "2019-02-19T16:03:22+00:00" }, { "name": "mikey179/vfsStream", @@ -7586,16 +7703,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": { @@ -7666,7 +7783,7 @@ "testing", "xunit" ], - "time": "2018-09-08T15:10:43+00:00" + "time": "2019-02-01T05:22:47+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -7725,6 +7842,7 @@ "mock", "xunit" ], + "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, { @@ -8468,16 +8586,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": { @@ -8521,20 +8639,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": { @@ -8584,7 +8702,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", @@ -8656,16 +8774,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": { @@ -8725,20 +8843,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": { @@ -8782,20 +8900,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": { @@ -8836,20 +8954,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": { @@ -8890,7 +9008,7 @@ "configuration", "options" ], - "time": "2018-11-11T19:52:12+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/polyfill-php70", @@ -9008,16 +9126,16 @@ }, { "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": { @@ -9054,20 +9172,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": { @@ -9113,7 +9231,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", @@ -9197,20 +9315,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" @@ -9218,7 +9337,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -9243,24 +9362,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", @@ -9293,7 +9413,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/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml index 404bebfb719dc..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 @@ -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/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/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..1419aff867d2d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php @@ -24,6 +24,7 @@ class CategoryProductsVariantsTest extends GraphQlAbstract */ public function testGetSimpleProductsFromCategory() { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/360'); $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..c5fd2c49b9924 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -70,21 +70,19 @@ 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); $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,16 +97,52 @@ 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( + 13, + $responseDataObject->getData('category/children/0/children/1/id') + ); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testGetCategoryById() + { + $rootCategoryId = 13; + $query = <<<QUERY +{ + category(id: {$rootCategoryId}) { + id + name + } +} +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); + $responseDataObject = new DataObject($response); + //Some sort of smoke testing self::assertEquals( - 5, - $responseDataObject->getData('category/children/7/children/1/children/0/id') + 'Category 1.2', + $responseDataObject->getData('category/name') + ); + self::assertEquals( + 13, + $responseDataObject->getData('category/id') ); } @@ -259,17 +293,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 +310,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 +321,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 +335,6 @@ public function testAnchorCategory() } } QUERY; - $response = $this->graphQlQuery($query); $expectedResponse = [ 'category' => [ @@ -331,7 +358,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 +391,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 +410,6 @@ private function assertWebsites($product, $actualResponse) 'is_default' => true, ] ]; - $this->assertEquals($actualResponse, $assertionMap); } @@ -410,7 +434,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..17c2af8dc59d0 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogInventory; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +class AddProductToCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::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->getMaskedQuoteId(); + $query = $this->getAddSimpleProductQuery($maskedQuoteId, $sku, $qty); + $this->graphQlQuery($query); + self::fail('Should be "The requested qty is not available" error message.'); + } + + /** + * @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->getMaskedQuoteId(); + $query = $this->getAddSimpleProductQuery($maskedQuoteId, $sku, $qty); + $this->graphQlQuery($query); + self::fail('Should be "The most you may purchase is 5." error message.'); + } + + /** + * @return string + */ + public function getMaskedQuoteId() : string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param int $qty + * @return string + */ + public function getAddSimpleProductQuery(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/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..c25eed1fd6511 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php @@ -28,6 +28,8 @@ class ConfigurableProductViewTest extends GraphQlAbstract */ public function testQueryConfigurableProductLinks() { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/361'); + $productSku = 'configurable'; $query @@ -204,7 +206,6 @@ public function testQueryConfigurableProductLinks() /** * @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); $product = $productRepository->get($productSku, false, null, true); @@ -407,6 +408,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..66b171740ccab 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php @@ -94,7 +94,7 @@ public function testChangeWeakPassword() /** * @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() { 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/UpdateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php index c11c1385f7412..ee8fabc43c901 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -47,33 +47,58 @@ 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)); + $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']); + $newDobDate = new \DateTime($newDob); + $this->assertEquals($newDobDate->format('Y-m-d'), $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']); } /** @@ -185,7 +210,7 @@ public function testUpdateEmailIfPasswordIsMissed() /** * @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() { 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..1ff0b53dda0bb --- /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_currecy_code + default_display_currecy_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_currecy_code', $result['currency']); + $this->assertArrayHasKey('default_display_currecy_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/Quote/AddConfigurableProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddConfigurableProductToCartTest.php new file mode 100644 index 0000000000000..b3f16c8734203 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddConfigurableProductToCartTest.php @@ -0,0 +1,155 @@ +<?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\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +class AddConfigurableProductToCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var Quote + */ + private $quote; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quote = $objectManager->create(Quote::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::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->getMaskedQuoteId(); + + $query = $this->getAddConfigurableProductMutationQuery($maskedQuoteId, $variantSku, $qty); + + $response = $this->graphQlQuery($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->getMaskedQuoteId(); + $query = $this->getAddConfigurableProductMutationQuery($maskedQuoteId, $variantSku, $qty); + + $this->graphQlQuery($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->getMaskedQuoteId(); + $query = $this->getAddConfigurableProductMutationQuery($maskedQuoteId, $variantSku, $qty); + + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getMaskedQuoteId() + { + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + return $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param int $qty + * + * @return string + */ + private function getAddConfigurableProductMutationQuery(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/Quote/AddSimpleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartTest.php new file mode 100644 index 0000000000000..3bda57381e9d1 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartTest.php @@ -0,0 +1,108 @@ +<?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\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +class AddSimpleProductToCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductToCart() + { + $sku = 'simple'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteId(); + + $query = $this->getAddSimpleProductQuery($maskedQuoteId, $sku, $qty); + $response = $this->graphQlQuery($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']); + } + + /** + * @return string + */ + public function getMaskedQuoteId() : string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param int $qty + * @return string + */ + public function getAddSimpleProductQuery(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/CouponTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CouponTest.php index 1f8ad06a9f8ed..54fdc3ac0f360 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CouponTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CouponTest.php @@ -58,7 +58,7 @@ public function testApplyCouponToGuestCartWithItems() $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); $response = $this->graphQlQuery($query); - self::assertArrayHasKey("applyCouponToCart", $response); + self::assertArrayHasKey('applyCouponToCart', $response); self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); } @@ -154,7 +154,7 @@ public function testRemoveCoupon() $response = $this->graphQlQuery($query); self::assertArrayHasKey('removeCouponFromCart', $response); - self::assertSame('', $response['removeCouponFromCart']['cart']['applied_coupon']['code']); + self::assertNull($response['removeCouponFromCart']['cart']['applied_coupon']['code']); } /** 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/CreateEmptyCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateEmptyCartTest.php new file mode 100644 index 0000000000000..0cb8a38b0cb5e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateEmptyCartTest.php @@ -0,0 +1,61 @@ +<?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\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; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->guestCartRepository = $objectManager->get(GuestCartRepositoryInterface::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateEmptyCart() + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + + $customerToken = $this->customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('createEmptyCart', $response); + + $maskedCartId = $response['createEmptyCart']; + $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/GetAvailablePaymentMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php new file mode 100644 index 0000000000000..5695aab6854d4 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +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 get available payment methods + */ +class GetAvailablePaymentMethodsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + 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); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + */ + public function testGetCartWithPaymentMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_item_with_items'); + + $query = <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + available_payment_methods { + code + title + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertEquals('checkmo', $response['cart']['available_payment_methods'][0]['code']); + self::assertEquals('Check / Money order', $response['cart']['available_payment_methods'][0]['title']); + } + + /** + * @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 $reversedQuoteId + * @return string + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} 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..fe9b7b3c49a7c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php @@ -0,0 +1,162 @@ +<?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\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 getting cart information + */ +class GetCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @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->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + */ + public function testGetCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_item_with_items'); + $query = $this->getCartQuery($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($response['cart']['items'][0]['qty'], 2); + self::assertEquals($response['cart']['items'][0]['product']['sku'], 'simple'); + + self::assertNotEmpty($response['cart']['items'][1]['id']); + self::assertEquals($response['cart']['items'][1]['qty'], 1); + self::assertEquals($response['cart']['items'][1]['product']['sku'], 'simple_one'); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testGetGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $query = $this->getCartQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + */ + public function testGetAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_item_with_items'); + $query = $this->getCartQuery($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->getCartQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getCartQuery( + string $maskedQuoteId + ) : string { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + items { + id + qty + product { + sku + } + } + } +} +QUERY; + } + + /** + * @param string $reversedQuoteId + * @return string + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @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..70ec646c10008 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php @@ -0,0 +1,265 @@ +<?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 removeItemFromCartTest mutation + */ +class RemoveItemFromCartTest 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 testRemoveItemFromCart() + { + $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(); + + $query = $this->prepareMutationQuery($maskedQuoteId, $itemId); + $response = $this->graphQlQuery($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->prepareMutationQuery('non_existent_masked_id', 1); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testRemoveNonExistentItem() + { + $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("Cart doesn't contain the {$notExistentItemId} item."); + + $query = $this->prepareMutationQuery($maskedQuoteId, $notExistentItemId); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testRemoveItemIfItemIsNotBelongToCart() + { + $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("Cart doesn't contain the {$secondQuoteItemId} item."); + + $query = $this->prepareMutationQuery($firstQuoteMaskedId, $secondQuoteItemId); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testRemoveItemFromGuestCart() + { + $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->prepareMutationQuery($guestQuoteMaskedId, $guestQuoteItemId); + $this->graphQlQuery($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 testRemoveItemFromAnotherCustomerCart() + { + $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->prepareMutationQuery($anotherCustomerQuoteMaskedId, $anotherCustomerQuoteItemId); + $this->graphQlQuery($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->graphQlQuery($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.' + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @return string + */ + private function prepareMutationQuery(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..1a93c011e80a8 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php @@ -0,0 +1,489 @@ +<?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\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 QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @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->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testSetNewBillingAddress() + { + $maskedQuoteId = $this->assignQuoteToCustomer(); + + $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->graphQlQuery($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/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testSetNewBillingAddressWithUseForShippingParameter() + { + $maskedQuoteId = $this->assignQuoteToCustomer(); + + $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->graphQlQuery($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/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + */ + public function testSetBillingAddressFromAddressBook() + { + $maskedQuoteId = $this->assignQuoteToCustomer(); + + $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->graphQlQuery($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/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a address with ID "100" + */ + public function testSetNotExistedBillingAddressFromAddressBook() + { + $maskedQuoteId = $this->assignQuoteToCustomer(); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 100 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @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 testSetNewBillingAddressAndFromAddressBookAtSameTime() + { + $maskedQuoteId = $this->assignQuoteToCustomer(); + + $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->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testSetBillingAddressToGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + + $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->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @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->graphQlQuery($query, [], '', $this->getHeaderMap('customer@search.example.com')); + } + + /** + * @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 The current user cannot use 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->graphQlQuery($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * 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 $reversedQuoteId + * @return string + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $reversedQuoteId + * @param int $customerId + * @return string + */ + private function assignQuoteToCustomer( + string $reversedQuoteId = 'test_order_with_simple_product_without_address', + int $customerId = 1 + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, '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/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php new file mode 100644 index 0000000000000..bbc77b6c39740 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php @@ -0,0 +1,220 @@ +<?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\OfflinePayments\Model\Checkmo; +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 setting payment methods on cart by customer + */ +class SetPaymentMethodOnCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + 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); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + */ + public function testSetPaymentWithVirtualProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_virtual_product'); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlQuery($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/Checkout/_files/quote_with_address_saved.php + */ + public function testSetPaymentWithSimpleProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlQuery($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/Checkout/_files/quote_with_simple_product_saved.php + * @expectedException \Exception + * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + */ + public function testSetPaymentWithSimpleProductWithoutAddress() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 1); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @expectedException \Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetNonExistingPaymentMethod() + { + $methodCode = 'noway'; + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testSetPaymentMethodToGuestCart() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testSetPaymentMethodToAnotherCustomerCart() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @param string $methodCode + * @return string + */ + private function prepareMutationQuery( + 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 $reversedQuoteId + * @return string + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $reversedQuoteId + * @param int $customerId + * @return string + */ + private function assignQuoteToCustomer( + string $reversedQuoteId, + int $customerId + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $quote->setCustomerId($customerId); + $this->quoteResource->save($quote); + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @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/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php new file mode 100644 index 0000000000000..736ea69440753 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php @@ -0,0 +1,206 @@ +<?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\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 setting shipping methods on cart for customer + */ +class SetShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + 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); + } + + public function testShippingMethodWithVirtualProduct() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testShippingMethodWithSimpleProduct() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testShippingMethodWithSimpleProductWithoutAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodWithMissedRequiredParameters() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetNonExistentShippingMethod() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodIfAddressIsNotBelongToCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToNonExistentCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToGuestCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToAnotherCustomerCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToNonExistentCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToGuestCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToAnotherCustomerCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetMultipleShippingMethods() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @param string $shippingAddressId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function prepareMutationQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode, + string $shippingAddressId + ) : string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_addresses: [{ + cart_address_id: $shippingAddressId + shipping_method: { + method_code: "$shippingMethodCode" + carrier_code: "$shippingCarrierCode" + } + }] + }) { + + cart { + cart_id, + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + label + amount + } + } + } + } +} + +QUERY; + } + + /** + * @param string $reversedQuoteId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $reversedQuoteId + * @param int $customerId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function assignQuoteToCustomer( + string $reversedQuoteId, + int $customerId + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $quote->setCustomerId($customerId); + $this->quoteResource->save($quote); + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $username + * @param string $password + * @return array + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + 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..632ee839834e0 --- /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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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/GetAvailableShippingMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetAvailableShippingMethodsTest.php new file mode 100644 index 0000000000000..1a0ccbd198c37 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetAvailableShippingMethodsTest.php @@ -0,0 +1,121 @@ +<?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 get available shipping methods + */ +class GetAvailableShippingMethodsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var Quote + */ + private $quote; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->quoteResource = $objectManager->create(QuoteResource::class); + $this->quote = $objectManager->create(Quote::class); + $this->quoteIdToMaskedId = $objectManager->create(QuoteIdToMaskedQuoteIdInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testGetAvailableShippingMethods() + { + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<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; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + 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] + ); + } + + /** + * @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/Quote/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartTest.php deleted file mode 100644 index 78204aaa567b0..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartTest.php +++ /dev/null @@ -1,170 +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 getting cart information - */ -class GetCartTest 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_items_saved.php - */ - public function testGetOwnCartForRegisteredCustomer() - { - $reservedOrderId = 'test_order_item_with_items'; - $this->quoteResource->load( - $this->quote, - $reservedOrderId, - 'reserved_order_id' - ); - - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $query = $this->prepareGetCartQuery($maskedQuoteId); - - $response = $this->sendRequestWithToken($query); - - self::assertArrayHasKey('Cart', $response); - self::assertNotEmpty($response['Cart']['items']); - self::assertNotEmpty($response['Cart']['addresses']); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php - */ - public function testGetCartFromAnotherCustomer() - { - $reservedOrderId = 'test_order_item_with_items'; - $this->quoteResource->load( - $this->quote, - $reservedOrderId, - 'reserved_order_id' - ); - - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $query = $this->prepareGetCartQuery($maskedQuoteId); - - self::expectExceptionMessage("The current user cannot perform operations on cart \"$maskedQuoteId\""); - - $this->graphQlQuery($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php - */ - public function testGetCartForGuest() - { - $reservedOrderId = 'test_order_1'; - $this->quoteResource->load( - $this->quote, - $reservedOrderId, - 'reserved_order_id' - ); - - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $query = $this->prepareGetCartQuery($maskedQuoteId); - - $response = $this->graphQlQuery($query); - - self::assertArrayHasKey('Cart', $response); - } - - public function testGetNonExistentCart() - { - $maskedQuoteId = 'non_existent_masked_id'; - $query = $this->prepareGetCartQuery($maskedQuoteId); - - self::expectExceptionMessage("Could not find a cart with ID \"$maskedQuoteId\""); - - $this->graphQlQuery($query); - } - - /** - * Generates query for setting the specified shipping method on cart - * - * @param string $maskedQuoteId - * @return string - */ - private function prepareGetCartQuery( - string $maskedQuoteId - ) : string { - return <<<QUERY -{ - Cart(cart_id: "$maskedQuoteId") { - applied_coupon { - code - } - items { - id - } - addresses { - firstname, - lastname, - address_type - } - } -} - -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/Quote/Guest/CreateEmptyCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php new file mode 100644 index 0000000000000..4fd398439913e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +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; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->guestCartRepository = $objectManager->get(GuestCartRepositoryInterface::class); + } + + public function testCreateEmptyCart() + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('createEmptyCart', $response); + + $maskedCartId = $response['createEmptyCart']; + $guestCart = $this->guestCartRepository->get($maskedCartId); + + self::assertNotNull($guestCart->getId()); + self::assertNull($guestCart->getCustomer()->getId()); + } +} 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..a5a08aaf39fb1 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php @@ -0,0 +1,89 @@ +<?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\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting cart information + */ +class GetAvailablePaymentMethodsTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testGetCartWithPaymentMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + + $query = <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + available_payment_methods { + code + title + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + + self::assertEquals('checkmo', $response['cart']['available_payment_methods'][0]['code']); + self::assertEquals('Check / Money order', $response['cart']['available_payment_methods'][0]['title']); + + self::assertEquals('free', $response['cart']['available_payment_methods'][1]['code']); + self::assertEquals( + 'No Payment Information Required', + $response['cart']['available_payment_methods'][1]['title'] + ); + } + + /** + * @param string $reversedQuoteId + * @return string + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} 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..b1d5e475c793e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php @@ -0,0 +1,148 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +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 getting cart information + */ +class GetCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @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->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + */ + public function testGetCart() + { + $maskedQuoteId = $this->unAssignCustomerFromQuote('test_order_item_with_items'); + $query = $this->getCartQuery($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($response['cart']['items'][0]['qty'], 2); + self::assertEquals($response['cart']['items'][0]['product']['sku'], 'simple'); + + self::assertNotEmpty($response['cart']['items'][1]['id']); + self::assertEquals($response['cart']['items'][1]['qty'], 1); + self::assertEquals($response['cart']['items'][1]['product']['sku'], 'simple_one'); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + */ + public function testGetCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_item_with_items'); + $query = $this->getCartQuery($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->getCartQuery($maskedQuoteId); + + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getCartQuery( + string $maskedQuoteId + ) : string { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + items { + id + qty + product { + sku + } + } + } +} +QUERY; + } + + /** + * @param string $reversedQuoteId + * @return string + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $reversedQuoteId + * @param int $customerId + * @return string + */ + private function unAssignCustomerFromQuote( + string $reversedQuoteId + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $quote->setCustomerId(0); + $this->quoteResource->save($quote); + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} 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..a306b29e51197 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.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\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 removeItemFromCartTest mutation + */ +class RemoveItemFromCartTest 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 testRemoveItemFromCart() + { + $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(); + + $query = $this->prepareMutationQuery($maskedQuoteId, $itemId); + $response = $this->graphQlQuery($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->prepareMutationQuery('non_existent_masked_id', 1); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testRemoveNonExistentItem() + { + $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("Cart doesn't contain the {$notExistentItemId} item."); + + $query = $this->prepareMutationQuery($maskedQuoteId, $notExistentItemId); + $this->graphQlQuery($query); + } + + /** + * Test mutation is only able to remove quote item belonging to the requested cart + * + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testRemoveItemIfItemIsNotBelongToCart() + { + $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("Cart doesn't contain the {$secondQuoteItemId} item."); + + $query = $this->prepareMutationQuery($firstQuoteMaskedId, $secondQuoteItemId); + $this->graphQlQuery($query); + } + + /** + * Test mutation is only able to remove quote item belonging to the requested cart + * + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testRemoveItemFromCustomerCart() + { + $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->prepareMutationQuery($customerQuoteMaskedId, $customerQuoteItemId); + $this->graphQlQuery($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->graphQlQuery($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.' + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @return string + */ + private function prepareMutationQuery(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..880d6aa0f406f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php @@ -0,0 +1,281 @@ +<?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\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 QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testSetNewBillingAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + + $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->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testSetNewBillingAddressWithUseForShippingParameter() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + + $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->graphQlQuery($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'); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testSettBillingAddressToCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + + $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->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testSetBillingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlQuery($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); + } + + /** + * @param string $reversedQuoteId + * @return string + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} 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..7484b2af7569d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.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\Guest; + +use Magento\OfflinePayments\Model\Checkmo; +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 setting payment methods on cart by guest + */ +class SetPaymentMethodOnCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + */ + public function testSetPaymentWithVirtualProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_virtual_product'); + $this->unAssignCustomerFromQuote('test_order_with_virtual_product'); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlQuery($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/Checkout/_files/quote_with_address_saved.php + */ + public function testSetPaymentWithSimpleProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + $this->unAssignCustomerFromQuote('test_order_1'); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlQuery($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/Checkout/_files/quote_with_simple_product_saved.php + * @expectedException \Exception + * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + */ + public function testSetPaymentWithSimpleProductWithoutAddress() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @expectedException \Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetNonExistingPaymentMethod() + { + $methodCode = 'noway'; + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + $this->unAssignCustomerFromQuote('test_order_1'); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testSetPaymentMethodToCustomerCart() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + + $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @param string $methodCode + * @return string + */ + private function prepareMutationQuery( + 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 $reversedQuoteId + * @param int $customerId + * @return string + */ + private function unAssignCustomerFromQuote( + string $reversedQuoteId + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $quote->setCustomerId(0); + $this->quoteResource->save($quote); + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $reversedQuoteId + * @return string + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} 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..f159cb6f6151e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php @@ -0,0 +1,167 @@ +<?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\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 for guest + */ +class SetShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + } + + public function testShippingMethodWithVirtualProduct() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testShippingMethodWithSimpleProduct() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testShippingMethodWithSimpleProductWithoutAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodWithMissedRequiredParameters() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetNonExistentShippingMethod() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodIfAddressIsNotBelongToCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToNonExistentCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToGuestCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToAnotherCustomerCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToNonExistentCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToGuestCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToAnotherCustomerCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetMultipleShippingMethods() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @param string $shippingAddressId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function prepareMutationQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode, + string $shippingAddressId + ) : string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_addresses: [{ + cart_address_id: $shippingAddressId + shipping_method: { + method_code: "$shippingMethodCode" + carrier_code: "$shippingCarrierCode" + } + }] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + label + amount + } + } + } + } +} + +QUERY; + } + + /** + * @param string $reversedQuoteId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} 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..fca7a4287620b --- /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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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 index a023d37895c23..4916bb2aa78c3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php @@ -7,8 +7,10 @@ namespace Magento\GraphQl\Quote; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Integration\Api\CustomerTokenServiceInterface; -use Magento\Quote\Model\Quote; +use Magento\Multishipping\Helper\Data; +use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; @@ -26,34 +28,35 @@ class SetShippingAddressOnCartTest extends GraphQlAbstract private $quoteResource; /** - * @var Quote + * @var QuoteFactory */ - private $quote; + private $quoteFactory; /** * @var QuoteIdToMaskedQuoteIdInterface */ private $quoteIdToMaskedId; + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + 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->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/Checkout/_files/quote_with_simple_product_saved.php */ - public function testSetNewGuestShippingAddressOnCart() + public function testSetNewShippingAddressByGuest() { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); $query = <<<QUERY mutation { @@ -79,7 +82,7 @@ public function testSetNewGuestShippingAddressOnCart() } ) { cart { - addresses { + shipping_addresses { firstname lastname company @@ -87,6 +90,11 @@ public function testSetNewGuestShippingAddressOnCart() city postcode telephone + country { + code + label + } + address_type } } } @@ -96,22 +104,19 @@ public function testSetNewGuestShippingAddressOnCart() self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); $cartResponse = $response['setShippingAddressesOnCart']['cart']; - self::assertArrayHasKey('addresses', $cartResponse); - $shippingAddressResponse = current($cartResponse['addresses']); + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); $this->assertNewShippingAddressFields($shippingAddressResponse); } /** * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. */ - public function testSetSavedShippingAddressOnCartByGuest() + public function testSetShippingAddressFromAddressBookByGuest() { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); $query = <<<QUERY mutation { @@ -126,34 +131,23 @@ public function testSetSavedShippingAddressOnCartByGuest() } ) { cart { - addresses { - firstname - lastname - company - street + shipping_addresses { 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 + * @magentoApiDataFixture Magento/Customer/_files/customer.php */ - public function testSetMultipleShippingAddressesOnCartByGuest() + public function testSetNewShippingAddressByRegisteredCustomer() { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $maskedQuoteId = $this->assignQuoteToCustomer(); $query = <<<QUERY mutation { @@ -162,16 +156,24 @@ public function testSetMultipleShippingAddressesOnCartByGuest() cart_id: "$maskedQuoteId" shipping_addresses: [ { - customer_address_id: 1 - }, - { - 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 { + shipping_addresses { firstname lastname company @@ -179,26 +181,33 @@ public function testSetMultipleShippingAddressesOnCartByGuest() city postcode telephone + country { + label + code + } + address_type } } } } QUERY; - self::expectExceptionMessage('You cannot specify multiple shipping addresses.'); - $this->graphQlQuery($query); + $response = $this->graphQlQuery($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/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php */ - public function testSetSavedAndNewShippingAddressOnCartAtTheSameTime() + public function testSetShippingAddressFromAddressBookByRegisteredCustomer() { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $maskedQuoteId = $this->assignQuoteToCustomer(); $query = <<<QUERY mutation { @@ -207,25 +216,13 @@ public function testSetSavedAndNewShippingAddressOnCartAtTheSameTime() 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 - } + customer_address_id: 1 } ] } ) { cart { - addresses { + shipping_addresses { firstname lastname company @@ -238,23 +235,56 @@ public function testSetSavedAndNewShippingAddressOnCartAtTheSameTime() } } QUERY; - self::expectExceptionMessage( - 'The shipping address cannot contain "customer_address_id" and "address" at the same time.' - ); - $this->graphQlQuery($query); + $response = $this->graphQlQuery($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/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a address with ID "100" */ - public function testSetShippingAddressOnCartWithNoAddresses() + public function testSetNotExistedShippingAddressFromAddressBook() { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $maskedQuoteId = $this->assignQuoteToCustomer(); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 100 + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @expectedException \Exception + * @expectedExceptionMessage The shipping address must contain either "customer_address_id" or "address". + */ + public function testSetShippingAddressWithoutAddresses() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); $query = <<<QUERY mutation { @@ -267,46 +297,71 @@ public function testSetShippingAddressOnCartWithNoAddresses() } ) { cart { - addresses { - firstname - lastname - company - street + shipping_addresses { 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 + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php */ - public function testSetNewRegisteredCustomerShippingAddressOnCart() + public function testSetNewShippingAddressAndFromAddressBookAtSameTime() { - $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' + $maskedQuoteId = $this->assignQuoteToCustomer(); + + $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->quote->setCustomerId(1); - $this->quoteResource->save($this->quote); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } - $headerMap = $this->getHeaderMap(); + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @expectedException \Exception + * @expectedExceptionMessage You cannot specify multiple shipping addresses. + */ + public function testSetMultipleNewShippingAddresses() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); $query = <<<QUERY mutation { @@ -327,55 +382,57 @@ public function testSetNewRegisteredCustomerShippingAddressOnCart() 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 { - addresses { - firstname - lastname - company - street + shipping_addresses { city - postcode - telephone } } } } QUERY; - $response = $this->graphQlQuery($query, [], '', $headerMap); + /** @var \Magento\Config\Model\ResourceModel\Config $config */ + $config = ObjectManager::getInstance()->get(\Magento\Config\Model\ResourceModel\Config::class); + $config->saveConfig( + Data::XML_PATH_CHECKOUT_MULTIPLE_AVAILABLE, + null, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = ObjectManager::getInstance()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->reinit(); - self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); - $cartResponse = $response['setShippingAddressesOnCart']['cart']; - self::assertArrayHasKey('addresses', $cartResponse); - $shippingAddressResponse = current($cartResponse['addresses']); - $this->assertNewShippingAddressFields($shippingAddressResponse); + $this->graphQlQuery($query); } /** + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @expectedException \Exception + * @expectedExceptionMessage The current user cannot use address with ID "1" */ - public function testSetSavedRegisteredCustomerShippingAddressOnCart() + public function testSetShippingAddressIfCustomerIsNotOwnerOfAddress() { - $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(); + $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 2); $query = <<<QUERY mutation { @@ -390,26 +447,15 @@ public function testSetSavedRegisteredCustomerShippingAddressOnCart() } ) { cart { - addresses { - firstname - lastname - company - street - city + shipping_addresses { 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); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer2@search.example.com')); } /** @@ -426,7 +472,9 @@ private function assertNewShippingAddressFields(array $shippingAddressResponse): ['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' => 'telephone', 'expected_value' => '88776655'], + ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ['response_field' => 'address_type', 'expected_value' => 'SHIPPING'] ]; $this->assertResponseFields($shippingAddressResponse, $assertionMap); @@ -454,16 +502,59 @@ private function assertSavedShippingAddressFields(array $shippingAddressResponse /** * @param string $username + * @param string $password * @return array */ - private function getHeaderMap(string $username = 'customer@example.com'): array + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array { - $password = 'password'; - /** @var CustomerTokenServiceInterface $customerTokenService */ - $customerTokenService = ObjectManager::getInstance() - ->get(CustomerTokenServiceInterface::class); - $customerToken = $customerTokenService->createCustomerAccessToken($username, $password); + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; return $headerMap; } + + /** + * @param string $reversedQuoteId + * @return string + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $reversedQuoteId + * @param int $customerId + * @return string + */ + private function assignQuoteToCustomer( + string $reversedQuoteId = 'test_order_with_simple_product_without_address', + int $customerId = 1 + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $quote->setCustomerId($customerId); + $this->quoteResource->save($quote); + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + public function tearDown() + { + /** @var \Magento\Config\Model\ResourceModel\Config $config */ + $config = ObjectManager::getInstance()->get(\Magento\Config\Model\ResourceModel\Config::class); + + //default state of multishipping config + $config->saveConfig( + Data::XML_PATH_CHECKOUT_MULTIPLE_AVAILABLE, + 1, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = ObjectManager::getInstance()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->reinit(); + } } 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..05e3e608c5e52 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php @@ -0,0 +1,357 @@ +<?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; + +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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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." + ); + + for ($i = 0; $i <= $sendFriend->getMaxSendsToFriend() + 1; $i++) { + $this->graphQlQuery($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/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/Vault/CustomerPaymentTokensTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Vault/CustomerPaymentTokensTest.php new file mode 100644 index 0000000000000..89fbbb9c49ed3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Vault/CustomerPaymentTokensTest.php @@ -0,0 +1,206 @@ +<?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; + +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->graphQlQuery($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->graphQlQuery($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->graphQlQuery($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/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/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/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/Util/Command/Cli.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli.php index 8fa22122cce89..f0abd280f3ebc 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli.php @@ -8,6 +8,7 @@ use Magento\Mtf\Util\Protocol\CurlInterface; use Magento\Mtf\Util\Protocol\CurlTransport; +use Magento\Mtf\Util\Protocol\CurlTransport\WebapiDecorator; /** * Perform bin/magento commands from command line for functional tests executions. @@ -17,7 +18,7 @@ class Cli /** * Url to command.php. */ - const URL = 'dev/tests/functional/utils/command.php'; + const URL = '/dev/tests/functional/utils/command.php'; /** * Curl transport protocol. @@ -26,12 +27,21 @@ class Cli */ private $transport; + /** + * Webapi handler. + * + * @var WebapiDecorator + */ + private $webapiHandler; + /** * @param CurlTransport $transport + * @param WebapiDecorator $webapiHandler */ - public function __construct(CurlTransport $transport) + public function __construct(CurlTransport $transport, WebapiDecorator $webapiHandler) { $this->transport = $transport; + $this->webapiHandler = $webapiHandler; } /** @@ -43,22 +53,31 @@ public function __construct(CurlTransport $transport) */ public function execute($command, $options = []) { - $curl = $this->transport; - $curl->write($this->prepareUrl($command, $options), [], CurlInterface::GET); - $curl->read(); - $curl->close(); + $this->transport->write( + rtrim(str_replace('index.php', '', $_ENV['app_frontend_url']), '/') . self::URL, + $this->prepareParamArray($command, $options), + CurlInterface::POST, + [] + ); + $this->transport->read(); + $this->transport->close(); } /** - * Prepare url. + * Prepare parameter array. * * @param string $command * @param array $options [optional] - * @return string + * @return array */ - private function prepareUrl($command, $options = []) + private function prepareParamArray($command, $options = []) { - $command .= ' ' . implode(' ', $options); - return $_ENV['app_frontend_url'] . self::URL . '?command=' . urlencode($command); + if (!empty($options)) { + $command .= ' ' . implode(' ', $options); + } + return [ + 'token' => urlencode($this->webapiHandler->getWebapiToken()), + 'command' => urlencode($command) + ]; } } 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..69df78a5cad64 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 @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Mtf\Util\Command\File\Export; use Magento\Mtf\ObjectManagerInterface; use Magento\Mtf\Util\Protocol\CurlTransport; use Magento\Mtf\Util\Protocol\CurlInterface; +use Magento\Mtf\Util\Protocol\CurlTransport\WebapiDecorator; /** * File reader for Magento export files. @@ -36,16 +36,29 @@ class Reader implements ReaderInterface */ private $transport; + /** + * Webapi handler. + * + * @var WebapiDecorator + */ + private $webapiHandler; + /** * @param ObjectManagerInterface $objectManager * @param CurlTransport $transport + * @param WebapiDecorator $webapiHandler * @param string $template */ - public function __construct(ObjectManagerInterface $objectManager, CurlTransport $transport, $template) - { + public function __construct( + ObjectManagerInterface $objectManager, + CurlTransport $transport, + WebapiDecorator $webapiHandler, + $template + ) { $this->objectManager = $objectManager; $this->template = $template; $this->transport = $transport; + $this->webapiHandler = $webapiHandler; } /** @@ -70,20 +83,27 @@ public function getData() */ private function getFiles() { - $this->transport->write($this->prepareUrl(), [], CurlInterface::GET); + $this->transport->write( + rtrim(str_replace('index.php', '', $_ENV['app_frontend_url']), '/') . self::URL, + $this->prepareParamArray(), + CurlInterface::POST, + [] + ); $serializedFiles = $this->transport->read(); $this->transport->close(); - return unserialize($serializedFiles); } /** - * Prepare url. + * Prepare parameter array. * - * @return string + * @return array */ - private function prepareUrl() + private function prepareParamArray() { - return $_ENV['app_frontend_url'] . self::URL . '?template=' . urlencode($this->template); + return [ + 'token' => urlencode($this->webapiHandler->getWebapiToken()), + 'template' => urlencode($this->template) + ]; } } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/ReaderInterface.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/ReaderInterface.php index 93f7cf1ce9764..3666e8643efa3 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/ReaderInterface.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/ReaderInterface.php @@ -14,7 +14,7 @@ interface ReaderInterface /** * Url to export.php. */ - const URL = 'dev/tests/functional/utils/export.php'; + const URL = '/dev/tests/functional/utils/export.php'; /** * Exporting files as Data object from Magento. 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..820a5b0a82228 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 @@ -7,6 +7,7 @@ namespace Magento\Mtf\Util\Command\File; use Magento\Mtf\Util\Protocol\CurlTransport; +use Magento\Mtf\Util\Protocol\CurlTransport\WebapiDecorator; /** * Get content of log file in var/log folder. @@ -16,7 +17,7 @@ class Log /** * Url to log.php. */ - const URL = 'dev/tests/functional/utils/log.php'; + const URL = '/dev/tests/functional/utils/log.php'; /** * Curl transport protocol. @@ -25,12 +26,21 @@ class Log */ private $transport; + /** + * Webapi handler. + * + * @var WebapiDecorator + */ + private $webapiHandler; + /** * @param CurlTransport $transport + * @param WebapiDecorator $webapiHandler */ - public function __construct(CurlTransport $transport) + public function __construct(CurlTransport $transport, WebapiDecorator $webapiHandler) { $this->transport = $transport; + $this->webapiHandler = $webapiHandler; } /** @@ -41,22 +51,28 @@ public function __construct(CurlTransport $transport) */ public function getFileContent($name) { - $curl = $this->transport; - $curl->write($this->prepareUrl($name), [], CurlTransport::GET); - $data = $curl->read(); - $curl->close(); - + $this->transport->write( + rtrim(str_replace('index.php', '', $_ENV['app_frontend_url']), '/') . self::URL, + $this->prepareParamArray($name), + CurlInterface::POST, + [] + ); + $data = $this->transport->read(); + $this->transport->close(); return unserialize($data); } /** - * Prepare url. + * Prepare parameter array. * * @param string $name - * @return string + * @return array */ - private function prepareUrl($name) + private function prepareParamArray($name) { - return $_ENV['app_frontend_url'] . self::URL . '?name=' . urlencode($name); + return [ + 'token' => urlencode($this->webapiHandler->getWebapiToken()), + 'name' => urlencode($name) + ]; } } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/GeneratedCode.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/GeneratedCode.php index dde3409ed1562..a9fefa25ffa24 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/GeneratedCode.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/GeneratedCode.php @@ -7,6 +7,7 @@ use Magento\Mtf\Util\Protocol\CurlInterface; use Magento\Mtf\Util\Protocol\CurlTransport; +use Magento\Mtf\Util\Protocol\CurlTransport\WebapiDecorator; /** * GeneratedCode removes generated code of Magento (like generated/code and generated/metadata). @@ -16,7 +17,7 @@ class GeneratedCode /** * Url to deleteMagentoGeneratedCode.php. */ - const URL = 'dev/tests/functional/utils/deleteMagentoGeneratedCode.php'; + const URL = '/dev/tests/functional/utils/deleteMagentoGeneratedCode.php'; /** * Curl transport protocol. @@ -25,12 +26,21 @@ class GeneratedCode */ private $transport; + /** + * Webapi handler. + * + * @var WebapiDecorator + */ + private $webapiHandler; + /** * @param CurlTransport $transport + * @param WebapiDecorator $webapiHandler */ - public function __construct(CurlTransport $transport) + public function __construct(CurlTransport $transport, WebapiDecorator $webapiHandler) { $this->transport = $transport; + $this->webapiHandler = $webapiHandler; } /** @@ -40,10 +50,25 @@ public function __construct(CurlTransport $transport) */ public function delete() { - $url = $_ENV['app_frontend_url'] . self::URL; - $curl = $this->transport; - $curl->write($url, [], CurlInterface::GET); - $curl->read(); - $curl->close(); + $this->transport->write( + rtrim(str_replace('index.php', '', $_ENV['app_frontend_url']), '/') . self::URL, + $this->prepareParamArray(), + CurlInterface::POST, + [] + ); + $this->transport->read(); + $this->transport->close(); + } + + /** + * Prepare parameter array. + * + * @return array + */ + private function prepareParamArray() + { + return [ + 'token' => urlencode($this->webapiHandler->getWebapiToken()) + ]; } } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Locales.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Locales.php index f669d91f2f2e5..a55d803f43087 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Locales.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Locales.php @@ -7,6 +7,7 @@ use Magento\Mtf\Util\Protocol\CurlInterface; use Magento\Mtf\Util\Protocol\CurlTransport; +use Magento\Mtf\Util\Protocol\CurlTransport\WebapiDecorator; /** * Returns array of locales depends on fetching type. @@ -26,7 +27,7 @@ class Locales /** * Url to locales.php. */ - const URL = 'dev/tests/functional/utils/locales.php'; + const URL = '/dev/tests/functional/utils/locales.php'; /** * Curl transport protocol. @@ -35,12 +36,21 @@ class Locales */ private $transport; + /** + * Webapi handler. + * + * @var WebapiDecorator + */ + private $webapiHandler; + /** * @param CurlTransport $transport Curl transport protocol + * @param WebapiDecorator $webapiHandler */ - public function __construct(CurlTransport $transport) + public function __construct(CurlTransport $transport, WebapiDecorator $webapiHandler) { $this->transport = $transport; + $this->webapiHandler = $webapiHandler; } /** @@ -51,12 +61,28 @@ public function __construct(CurlTransport $transport) */ public function getList($type = self::TYPE_ALL) { - $url = $_ENV['app_frontend_url'] . self::URL . '?type=' . $type; - $curl = $this->transport; - $curl->write($url, [], CurlInterface::GET); - $result = $curl->read(); - $curl->close(); - + $this->transport->write( + rtrim(str_replace('index.php', '', $_ENV['app_frontend_url']), '/') . self::URL, + $this->prepareParamArray($type), + CurlInterface::POST, + [] + ); + $result = $this->transport->read(); + $this->transport->close(); return explode('|', $result); } + + /** + * Prepare parameter array. + * + * @param string $type + * @return array + */ + private function prepareParamArray($type) + { + return [ + 'token' => urlencode($this->webapiHandler->getWebapiToken()), + 'type' => urlencode($type) + ]; + } } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/PathChecker.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/PathChecker.php index fd1f746a6f09c..4b12f6eec87aa 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/PathChecker.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/PathChecker.php @@ -7,6 +7,7 @@ use Magento\Mtf\Util\Protocol\CurlInterface; use Magento\Mtf\Util\Protocol\CurlTransport; +use Magento\Mtf\Util\Protocol\CurlTransport\WebapiDecorator; /** * PathChecker checks that path to file or directory exists. @@ -16,7 +17,7 @@ class PathChecker /** * Url to checkPath.php. */ - const URL = 'dev/tests/functional/utils/pathChecker.php'; + const URL = '/dev/tests/functional/utils/pathChecker.php'; /** * Curl transport protocol. @@ -26,11 +27,21 @@ class PathChecker private $transport; /** + * Webapi handler. + * + * @var WebapiDecorator + */ + private $webapiHandler; + + /** + * @constructor * @param CurlTransport $transport + * @param WebapiDecorator $webapiHandler */ - public function __construct(CurlTransport $transport) + public function __construct(CurlTransport $transport, WebapiDecorator $webapiHandler) { $this->transport = $transport; + $this->webapiHandler = $webapiHandler; } /** @@ -41,12 +52,28 @@ public function __construct(CurlTransport $transport) */ public function pathExists($path) { - $url = $_ENV['app_frontend_url'] . self::URL . '?path=' . urlencode($path); - $curl = $this->transport; - $curl->write($url, [], CurlInterface::GET); - $result = $curl->read(); - $curl->close(); - + $this->transport->write( + rtrim(str_replace('index.php', '', $_ENV['app_frontend_url']), '/') . self::URL, + $this->prepareParamArray($path), + CurlInterface::POST, + [] + ); + $result = $this->transport->read(); + $this->transport->close(); return strpos($result, 'path exists: true') !== false; } + + /** + * Prepare parameter array. + * + * @param string $path + * @return array + */ + private function prepareParamArray($path) + { + return [ + 'token' => urlencode($this->webapiHandler->getWebapiToken()), + 'path' => urlencode($path) + ]; + } } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Website.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Website.php index 7d73634c0360d..fec20bb2a8715 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Website.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Website.php @@ -3,11 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Mtf\Util\Command; use Magento\Mtf\Util\Protocol\CurlInterface; use Magento\Mtf\Util\Protocol\CurlTransport; +use Magento\Mtf\Util\Protocol\CurlTransport\WebapiDecorator; /** * Perform Website folder creation for functional tests executions. @@ -17,7 +17,7 @@ class Website /** * Url to website.php. */ - const URL = 'dev/tests/functional/utils/website.php'; + const URL = '/dev/tests/functional/utils/website.php'; /** * Curl transport protocol. @@ -26,13 +26,22 @@ class Website */ private $transport; + /** + * Webapi handler. + * + * @var WebapiDecorator + */ + private $webapiHandler; + /** * @constructor * @param CurlTransport $transport + * @param WebapiDecorator $webapiHandler */ - public function __construct(CurlTransport $transport) + public function __construct(CurlTransport $transport, WebapiDecorator $webapiHandler) { $this->transport = $transport; + $this->webapiHandler = $webapiHandler; } /** @@ -43,21 +52,28 @@ public function __construct(CurlTransport $transport) */ public function create($websiteCode) { - $curl = $this->transport; - $curl->addOption(CURLOPT_HEADER, 1); - $curl->write($this->prepareUrl($websiteCode), [], CurlInterface::GET); - $curl->read(); - $curl->close(); + $this->transport->addOption(CURLOPT_HEADER, 1); + $this->transport->write( + rtrim(str_replace('index.php', '', $_ENV['app_frontend_url']), '/') . self::URL, + $this->prepareParamArray($websiteCode), + CurlInterface::POST, + [] + ); + $this->transport->read(); + $this->transport->close(); } /** - * Prepare url. + * Prepare parameter array. * * @param string $websiteCode - * @return string + * @return array */ - private function prepareUrl($websiteCode) + private function prepareParamArray($websiteCode) { - return $_ENV['app_frontend_url'] . self::URL . '?website_code=' . urlencode($websiteCode); + return [ + 'token' => urlencode($this->webapiHandler->getWebapiToken()), + 'website_code' => urlencode($websiteCode) + ]; } } 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..d7026e9b8efb3 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 @@ -63,23 +63,56 @@ public function __construct(CurlTransport $transport, DataInterface $configurati */ protected function authorize() { - // Perform GET to backend url so form_key is set - $url = $_ENV['app_backend_url']; - $this->transport->write($url, [], CurlInterface::GET); - $this->read(); - - $url = $_ENV['app_backend_url'] . $this->configuration->get('application/0/backendLoginUrl/0/value'); - $data = [ - 'login[username]' => $this->configuration->get('application/0/backendLogin/0/value'), - 'login[password]' => $this->configuration->get('application/0/backendPassword/0/value'), - 'form_key' => $this->formKey, - ]; - $this->transport->write($url, $data, CurlInterface::POST); - $response = $this->read(); - if (strpos($response, 'login-form') !== false) { - throw new \Exception( - 'Admin user cannot be logged in by curl handler!' - ); + // There are situations where magento application backend url could be slightly different from the environment + // variable we know. It could be intentionally (e.g. InstallTest) or unintentionally. We would still want tests + // to run in this case. + // When the original app_backend_url does not work, we will try 4 variants of the it. i.e. with and without + // url rewrite, http and https. + $urls = []; + $originalUrl = rtrim($_ENV['app_backend_url'], '/') . '/'; + $urls[] = $originalUrl; + // It could be the case that the page needs a refresh, so we will try the original one twice. + $urls[] = $originalUrl; + if (strpos($originalUrl, '/index.php') !== false) { + $url2 = str_replace('/index.php', '', $originalUrl); + } else { + $url2 = $originalUrl . 'index.php/'; + } + $urls[] = $url2; + if (strpos($originalUrl, 'https') !== false) { + $urls[] = str_replace('https', 'http', $originalUrl); + } else { + $urls[] = str_replace('http', 'https', $url2); + } + + $isAuthorized = false; + foreach ($urls as $url) { + try { + // Perform GET to backend url so form_key is set + $this->transport->write($url, [], CurlInterface::GET); + $this->read(); + + $authUrl = $url . $this->configuration->get('application/0/backendLoginUrl/0/value'); + $data = [ + 'login[username]' => $this->configuration->get('application/0/backendLogin/0/value'), + 'login[password]' => $this->configuration->get('application/0/backendPassword/0/value'), + 'form_key' => $this->formKey, + ]; + + $this->transport->write($authUrl, $data, CurlInterface::POST); + $response = $this->read(); + if (strpos($response, 'login-form') !== false) { + continue; + } + $isAuthorized = true; + $_ENV['app_backend_url'] = $url; + break; + } catch (\Exception $e) { + continue; + } + } + if ($isAuthorized == false) { + throw new \Exception('Admin user cannot be logged in by curl handler!'); } } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/WebapiDecorator.php b/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/WebapiDecorator.php index 3aa756904ab00..df5ab45a3f96d 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/WebapiDecorator.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/WebapiDecorator.php @@ -70,6 +70,13 @@ class WebapiDecorator implements CurlInterface */ protected $response; + /** + * Webapi token. + * + * @var string + */ + protected $webapiToken; + /** * @construct * @param ObjectManager $objectManager @@ -110,6 +117,9 @@ protected function init() $integration->persist(); $this->setConfiguration($integration); + $this->webapiToken = $integration->getToken(); + } else { + $this->webapiToken = $integrationToken; } } @@ -161,7 +171,13 @@ protected function setConfiguration(Integration $integration) */ protected function isValidIntegration() { - $this->write($_ENV['app_frontend_url'] . 'rest/V1/modules', [], CurlInterface::GET); + $url = rtrim($_ENV['app_frontend_url'], '/'); + if (strpos($url, 'index.php') === false) { + $url .= '/index.php/rest/V1/modules'; + } else { + $url .= '/rest/V1/modules'; + } + $this->write($url, [], CurlInterface::GET); $response = json_decode($this->read(), true); return (null !== $response) && !isset($response['message']); @@ -219,4 +235,18 @@ public function close() { $this->transport->close(); } + + /** + * Return webapiToken. + * + * @return string + */ + public function getWebapiToken() + { + // Request token if integration is no longer valid + if (!$this->isValidIntegration()) { + $this->init(); + } + return $this->webapiToken; + } } 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..3020e69c06399 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,16 @@ class ExportAdvancedPricingTest extends Injectable private $catalogProductIndex; /** - * Prepare test data. + * Run cron before tests running * - * @param CatalogProductIndex $catalogProductIndex + * @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(); } /** @@ -132,7 +133,7 @@ public function test( } $products = $this->prepareProducts($products, $website); $this->adminExportIndex->open(); - + $this->adminExportIndex->getExportedGrid()->deleteAllExportedFiles(); $exportData = $this->fixtureFactory->createByCode( 'exportData', [ @@ -191,6 +192,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..d069499da4aab 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"> @@ -49,6 +50,7 @@ <constraint name="Magento\AdvancedPricingImportExport\Test\Constraint\AssertExportAdvancedPricing"/> </variation> <variation name="ExportAdvancedPricingTestVariation4" summary="Trying export product data for product available on main website with default currency and custom website with different currency" ticketId="MAGETWO-46153"> + <data name="issue" xsi:type="string">MC-13864 Consumer always read config from memory</data> <data name="configData" xsi:type="string">price_scope_website</data> <data name="exportData" xsi:type="string">csv_with_advanced_pricing</data> <data name="products/0" xsi:type="array"> 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/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/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/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/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/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/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/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/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/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/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..2287546aed102 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 @@ -20,6 +20,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> @@ -115,7 +116,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 +155,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> @@ -209,6 +211,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..11ba7266ce564 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" /> 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/UpdateProductAttributeEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateProductAttributeEntityTest.xml index b051d50b4acb6..40cf8e40ae33f 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 @@ -67,6 +67,7 @@ <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 +77,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..e55558482c1f3 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,16 @@ 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->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..b94f21371496a 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 @@ -58,6 +58,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="issue" xsi:type="string">>MC-13864 Consumer always read config from memory</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/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/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/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/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml index c157f5c58d408..93240586ec92c 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,47 @@ </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="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="issue" xsi:type="string">>MC-13864 Consumer always read config from memory</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/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/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/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/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/CustomerImportExport/Test/TestCase/ExportCustomerAddressesTest.php b/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/TestCase/ExportCustomerAddressesTest.php index 1f046f5111dfe..6b92891ada2b4 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,8 +79,11 @@ 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(); 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/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/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/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/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/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/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/Ui/Test/Block/Adminhtml/DataGrid.php b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php index 03476add669be..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. * @@ -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/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/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/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/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/authenticate.php b/dev/tests/functional/utils/authenticate.php new file mode 100644 index 0000000000000..15851f6e8000a --- /dev/null +++ b/dev/tests/functional/utils/authenticate.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * Check if token passed in is a valid auth token. + * + * @param string $token + * @return bool + */ +function authenticate($token) +{ + require_once __DIR__ . '/../../../../app/bootstrap.php'; + + $magentoObjectManagerFactory = \Magento\Framework\App\Bootstrap::createObjectManagerFactory(BP, $_SERVER); + $magentoObjectManager = $magentoObjectManagerFactory->create($_SERVER); + $tokenModel = $magentoObjectManager->get(\Magento\Integration\Model\Oauth\Token::class); + + $tokenPassedIn = $token; + // Token returned will be null if the token we passed in is invalid + $tokenFromMagento = $tokenModel->loadByToken($tokenPassedIn)->getToken(); + if (!empty($tokenFromMagento) && ($tokenFromMagento == $tokenPassedIn)) { + return true; + } else { + return false; + } +} diff --git a/dev/tests/functional/utils/command.php b/dev/tests/functional/utils/command.php index 8eaf82475a4e4..4e18598a935ad 100644 --- a/dev/tests/functional/utils/command.php +++ b/dev/tests/functional/utils/command.php @@ -3,21 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - +include __DIR__ . '/authenticate.php'; require_once __DIR__ . '/../../../../app/bootstrap.php'; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\NullOutput; -if (isset($_GET['command'])) { - $command = urldecode($_GET['command']); - $magentoObjectManagerFactory = \Magento\Framework\App\Bootstrap::createObjectManagerFactory(BP, $_SERVER); - $magentoObjectManager = $magentoObjectManagerFactory->create($_SERVER); - $cli = $magentoObjectManager->create(\Magento\Framework\Console\Cli::class); - $input = new StringInput($command); - $input->setInteractive(false); - $output = new NullOutput(); - $cli->doRun($input, $output); +if (!empty($_POST['token']) && !empty($_POST['command'])) { + if (authenticate(urldecode($_POST['token']))) { + $command = urldecode($_POST['command']); + $magentoObjectManagerFactory = \Magento\Framework\App\Bootstrap::createObjectManagerFactory(BP, $_SERVER); + $magentoObjectManager = $magentoObjectManagerFactory->create($_SERVER); + $cli = $magentoObjectManager->create(\Magento\Framework\Console\Cli::class); + $input = new StringInput(escapeshellcmd($command)); + $input->setInteractive(false); + $output = new NullOutput(); + $cli->doRun($input, $output); + } else { + echo "Command not unauthorized."; + } } else { - throw new \InvalidArgumentException("Command GET parameter is not set."); + echo "'token' or 'command' parameter is not set."; } diff --git a/dev/tests/functional/utils/deleteMagentoGeneratedCode.php b/dev/tests/functional/utils/deleteMagentoGeneratedCode.php index 99aa9af06e92a..17e3575c87686 100644 --- a/dev/tests/functional/utils/deleteMagentoGeneratedCode.php +++ b/dev/tests/functional/utils/deleteMagentoGeneratedCode.php @@ -3,5 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +include __DIR__ . '/authenticate.php'; -exec('rm -rf ../../../../generated/*'); +if (!empty($_POST['token']) && !empty($_POST['path'])) { + if (authenticate(urldecode($_POST['token']))) { + exec('rm -rf ../../../../generated/*'); + } else { + echo "Command not unauthorized."; + } +} else { + echo "'token' parameter is not set."; +} diff --git a/dev/tests/functional/utils/export.php b/dev/tests/functional/utils/export.php index 343dcc557c832..e3eff6e3fec17 100644 --- a/dev/tests/functional/utils/export.php +++ b/dev/tests/functional/utils/export.php @@ -3,25 +3,30 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +include __DIR__ . '/authenticate.php'; -if (!isset($_GET['template'])) { - throw new \InvalidArgumentException('Argument "template" must be set.'); -} +if (!empty($_POST['token']) && !empty($_POST['template'])) { + if (authenticate(urldecode($_POST['token']))) { + $varDir = '../../../../var/export/'; + $template = urldecode($_POST['template']); + $fileList = scandir($varDir, SCANDIR_SORT_NONE); + $files = []; -$varDir = '../../../../var/'; -$template = urldecode($_GET['template']); -$fileList = scandir($varDir, SCANDIR_SORT_NONE); -$files = []; + foreach ($fileList as $fileName) { + if (preg_match("`$template`", $fileName) === 1) { + $filePath = $varDir . $fileName; + $files[] = [ + 'content' => file_get_contents($filePath), + 'name' => $fileName, + 'date' => filectime($filePath), + ]; + } + } -foreach ($fileList as $fileName) { - if (preg_match("`$template`", $fileName) === 1) { - $filePath = $varDir . $fileName; - $files[] = [ - 'content' => file_get_contents($filePath), - 'name' => $fileName, - 'date' => filectime($filePath), - ]; + echo serialize($files); + } else { + echo "Command not unauthorized."; } +} else { + echo "'token' or 'template' parameter is not set."; } - -echo serialize($files); diff --git a/dev/tests/functional/utils/locales.php b/dev/tests/functional/utils/locales.php index 827b8b1b89448..a3b4ec05eed65 100644 --- a/dev/tests/functional/utils/locales.php +++ b/dev/tests/functional/utils/locales.php @@ -3,15 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +include __DIR__ . '/authenticate.php'; -if (isset($_GET['type']) && $_GET['type'] == 'deployed') { - $themePath = isset($_GET['theme_path']) ? $_GET['theme_path'] : 'adminhtml/Magento/backend'; - $directory = __DIR__ . '/../../../../pub/static/' . $themePath; - $locales = array_diff(scandir($directory), ['..', '.']); +if (!empty($_POST['token'])) { + if (authenticate(urldecode($_POST['token']))) { + if ($_POST['type'] == 'deployed') { + $themePath = isset($_POST['theme_path']) ? $_POST['theme_path'] : 'adminhtml/Magento/backend'; + $directory = __DIR__ . '/../../../../pub/static/' . $themePath; + $locales = array_diff(scandir($directory), ['..', '.']); + } else { + require_once __DIR__ . DIRECTORY_SEPARATOR . 'bootstrap.php'; + $localeConfig = $magentoObjectManager->create(\Magento\Framework\Locale\Config::class); + $locales = $localeConfig->getAllowedLocales(); + } + echo implode('|', $locales); + } else { + echo "Command not unauthorized."; + } } else { - require_once __DIR__ . DIRECTORY_SEPARATOR . 'bootstrap.php'; - $localeConfig = $magentoObjectManager->create(\Magento\Framework\Locale\Config::class); - $locales = $localeConfig->getAllowedLocales(); + echo "'token' parameter is not set."; } - -echo implode('|', $locales); diff --git a/dev/tests/functional/utils/log.php b/dev/tests/functional/utils/log.php index 68a68d4bad648..889056bfbdd63 100644 --- a/dev/tests/functional/utils/log.php +++ b/dev/tests/functional/utils/log.php @@ -3,17 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); +include __DIR__ . '/authenticate.php'; -if (!isset($_GET['name'])) { - throw new \InvalidArgumentException( - 'The name of log file is required for getting logs.' - ); -} -$name = urldecode($_GET['name']); -if (preg_match('/\.\.(\\\|\/)/', $name)) { - throw new \InvalidArgumentException('Invalid log file name'); -} +if (!empty($_POST['token']) && !empty($_POST['name'])) { + if (authenticate(urldecode($_POST['token']))) { + $name = urldecode($_POST['name']); + if (preg_match('/\.\.(\\\|\/)/', $name)) { + throw new \InvalidArgumentException('Invalid log file name'); + } -echo serialize(file_get_contents('../../../../var/log' .'/' .$name)); + echo serialize(file_get_contents('../../../../var/log' . '/' . $name)); + } else { + echo "Command not unauthorized."; + } +} else { + echo "'token' or 'name' parameter is not set."; +} diff --git a/dev/tests/functional/utils/pathChecker.php b/dev/tests/functional/utils/pathChecker.php index 11f8229bce56f..b5a2ddb405bde 100644 --- a/dev/tests/functional/utils/pathChecker.php +++ b/dev/tests/functional/utils/pathChecker.php @@ -3,15 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +include __DIR__ . '/authenticate.php'; -if (isset($_GET['path'])) { - $path = urldecode($_GET['path']); +if (!empty($_POST['token']) && !empty($_POST['path'])) { + if (authenticate(urldecode($_POST['token']))) { + $path = urldecode($_POST['path']); - if (file_exists('../../../../' . $path)) { - echo 'path exists: true'; + if (file_exists('../../../../' . $path)) { + echo 'path exists: true'; + } else { + echo 'path exists: false'; + } } else { - echo 'path exists: false'; + echo "Command not unauthorized."; } } else { - throw new \InvalidArgumentException("GET parameter 'path' is not set."); + echo "'token' or 'path' parameter is not set."; } diff --git a/dev/tests/functional/utils/website.php b/dev/tests/functional/utils/website.php index 625f5c6b483f8..ab8e3742f55ae 100644 --- a/dev/tests/functional/utils/website.php +++ b/dev/tests/functional/utils/website.php @@ -3,30 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +include __DIR__ . '/authenticate.php'; -if (!isset($_GET['website_code'])) { - throw new \Exception("website_code GET parameter is not set."); -} - -$websiteCode = urldecode($_GET['website_code']); -$rootDir = '../../../../'; -$websiteDir = $rootDir . 'websites/' . $websiteCode . '/'; -$contents = file_get_contents($rootDir . 'index.php'); +if (!empty($_POST['token']) && !empty($_POST['website_code'])) { + if (authenticate(urldecode($_POST['token']))) { + $websiteCode = urldecode($_POST['website_code']); + $rootDir = '../../../../'; + $websiteDir = $rootDir . 'websites/' . $websiteCode . '/'; + $contents = file_get_contents($rootDir . 'index.php'); -$websiteParam = <<<EOD + $websiteParam = <<<EOD \$params = \$_SERVER; \$params[\Magento\Store\Model\StoreManager::PARAM_RUN_CODE] = '$websiteCode'; \$params[\Magento\Store\Model\StoreManager::PARAM_RUN_TYPE] = 'website'; EOD; -$pattern = '`(try {.*?)(\/app\/bootstrap.*?}\n)(.*?)\$_SERVER`mis'; -$replacement = "$1/../..$2\n$websiteParam$3\$params"; + $pattern = '`(try {.*?)(\/app\/bootstrap.*?}\n)(.*?)\$_SERVER`mis'; + $replacement = "$1/../..$2\n$websiteParam$3\$params"; -$contents = preg_replace($pattern, $replacement, $contents); + $contents = preg_replace($pattern, $replacement, $contents); -$old = umask(0); -mkdir($websiteDir, 0760, true); -umask($old); + $old = umask(0); + mkdir($websiteDir, 0760, true); + umask($old); -copy($rootDir . '.htaccess', $websiteDir . '.htaccess'); -file_put_contents($websiteDir . 'index.php', $contents); + copy($rootDir . '.htaccess', $websiteDir . '.htaccess'); + file_put_contents($websiteDir . 'index.php', $contents); + } else { + echo "Command not unauthorized."; + } +} else { + echo "'token' or 'website_code' parameter is not set."; +} diff --git a/dev/tests/integration/etc/di/preferences/ce.php b/dev/tests/integration/etc/di/preferences/ce.php index f14fad963c625..3065b1712640b 100644 --- a/dev/tests/integration/etc/di/preferences/ce.php +++ b/dev/tests/integration/etc/di/preferences/ce.php @@ -27,5 +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\Session\SessionStartChecker::class => \Magento\TestFramework\Session\SessionStartChecker::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..f0ee8c7b3c2bb 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,49 @@ 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) + { + $fixturePathParts = explode('::', $fixture, 2); + $moduleName = $fixturePathParts[0]; + $fixtureFile = $fixturePathParts[1]; + + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var ComponentRegistrar $componentRegistrar */ + $componentRegistrar = $objectManager->get(ComponentRegistrar::class); + $modulePath = $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 . '/' . $fixtureFile; + } + + /** + * Get method annotations. + * + * Overwrites class-defined annotations. + * * @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..9ca351aa1cf98 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,11 +88,8 @@ 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); @@ -108,6 +118,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 +152,8 @@ public function stopConsumers() } /** + * Get Consumers ProcessIds + * * @return array */ public function getConsumersProcessIds() @@ -133,6 +166,8 @@ public function getConsumersProcessIds() } /** + * Get Consumer ProcessIds + * * @param string $consumer * @return string[] */ @@ -167,8 +202,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,6 +222,8 @@ public function waitForAsynchronousResult(callable $condition, $params) } /** + * Get publisher + * * @return PublisherInterface */ public function getPublisher() 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/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/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/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/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/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/_files/enable_all_shipping_methods.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods.php new file mode 100644 index 0000000000000..dd48975aa2b09 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +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->create(WriterInterface::class); + +$configWriter->save('carriers/flatrate/active', 1); +$configWriter->save('carriers/tablerate/active', 1); +$configWriter->save('carriers/freeshipping/active', 1); +$configWriter->save('carriers/ups/active', 1); +$configWriter->save('carriers/usps/active', 1); +$configWriter->save('carriers/fedex/active', 1); +$configWriter->save('carriers/dhl/active', 1); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods_rollback.php new file mode 100644 index 0000000000000..7a3ca79febf6d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods_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\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'); +$configWriter->delete('carriers/ups/active'); +$configWriter->delete('carriers/usps/active'); +$configWriter->delete('carriers/fedex/active'); +$configWriter->delete('carriers/dhl/active'); 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/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/_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/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/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/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/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/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/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/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/GraphQl/Controller/GraphQlControllerTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php index 16a15cfcd2e26..384892d6fd5d2 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php @@ -116,7 +116,6 @@ public function testDispatch() : void */ public function testError() : void { - $this->markTestSkipped('Causes failiure with php unit and php 7.2'); $query = <<<QUERY { 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/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/Model/Config/Structure/Reader/_files/expected/config.xml b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml index 2bd346a6e8f7b..222b9974177de 100644 --- 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 @@ -347,7 +347,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>]]> @@ -358,7 +358,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> @@ -640,6 +640,7 @@ </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]]> @@ -727,7 +728,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®. @@ -740,7 +741,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> @@ -1214,6 +1215,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> @@ -1258,7 +1515,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>]]> @@ -1533,7 +1790,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>]]> @@ -1544,7 +1801,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> @@ -1816,6 +2073,8 @@ <field id="enable_paypal_payflow"> <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> </field> + <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> + <field id="merchant_id" showInDefault="0" showInWebsite="0"/> <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"> @@ -1830,8 +2089,6 @@ <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. @@ -1845,7 +2102,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/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/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php index b289e9b94558e..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,7 +5,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; @@ -23,7 +22,10 @@ use PHPUnit\Framework\MockObject\MockObject; /** + * Class for test Account + * * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountTest extends \PHPUnit\Framework\TestCase { @@ -43,23 +45,33 @@ class AccountTest extends \PHPUnit\Framework\TestCase private $session; /** - * @magentoDataFixture Magento/Sales/_files/quote.php + * @inheritdoc */ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $quote = $this->objectManager->create(Quote::class)->load(1); + parent::setUp(); + } + + /** + * Test for get form with existing customer + * + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testGetFormWithCustomer() + { + $customerGroup = 2; + $quote = $this->objectManager->create(Quote::class); $this->session = $this->getMockBuilder(SessionQuote::class) ->disableOriginalConstructor() - ->setMethods(['getCustomerId', 'getStore', 'getStoreId', 'getQuote', 'getQuoteId']) + ->setMethods(['getCustomerId','getQuote']) ->getMock(); - $this->session->method('getCustomerId') - ->willReturn(1); $this->session->method('getQuote') ->willReturn($quote); - $this->session->method('getQuoteId') - ->willReturn($quote->getId()); + $this->session->method('getCustomerId') + ->willReturn(1); + /** @var LayoutInterface $layout */ $layout = $this->objectManager->get(LayoutInterface::class); $this->accountBlock = $layout->createBlock( @@ -67,18 +79,20 @@ protected function setUp() 'address_block' . rand(), ['sessionQuote' => $this->session] ); - parent::setUp(); - } - /** - * @magentoDataFixture Magento/Customer/_files/customer.php - */ - public function testGetForm() - { + $fixtureCustomerId = 1; + /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->objectManager->get(\Magento\Customer\Api\CustomerRepositoryInterface::class); + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $customerRepository->getById($fixtureCustomerId); + $customer->setGroupId($customerGroup); + $customerRepository->save($customer); + $expectedFields = ['group_id', 'email']; $form = $this->accountBlock->getForm(); self::assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); $fieldset = $form->getElements()[0]; + $content = $form->toHtml(); self::assertEquals(count($expectedFields), $fieldset->getElements()->count()); @@ -88,22 +102,45 @@ public function testGetForm() sprintf('Unexpected field "%s" in form.', $element->getId()) ); } + + self::assertContains( + '<option value="'.$customerGroup.'" selected="selected">Wholesale</option>', + $content, + 'The Customer Group specified for the chosen customer should be selected.' + ); + + self::assertContains( + 'value="'.$customer->getEmail().'"', + $content, + 'The Customer Email specified for the chosen customer should be input ' + ); } /** * Tests a case when user defined custom attribute has default value. * - * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoConfigFixture current_store customer/create_account/default_group 3 + * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php + * @magentoConfigFixture current_store customer/create_account/default_group 2 + * @magentoConfigFixture secondstore_store customer/create_account/default_group 3 */ public function testGetFormWithUserDefinedAttribute() { + /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ + $storeManager = Bootstrap::getObjectManager()->get(\Magento\Store\Model\StoreManagerInterface::class); + $secondStore = $storeManager->getStore('secondstore'); + + $quoteSession = $this->objectManager->get(SessionQuote::class); + $quoteSession->setStoreId($secondStore->getId()); + $formFactory = $this->getFormFactoryMock(); $this->objectManager->addSharedInstance($formFactory, FormFactory::class); /** @var LayoutInterface $layout */ $layout = $this->objectManager->get(LayoutInterface::class); - $accountBlock = $layout->createBlock(Account::class, 'address_block' . rand()); + $accountBlock = $layout->createBlock( + Account::class, + 'address_block' . rand() + ); $form = $accountBlock->getForm(); $form->setUseContainer(true); @@ -116,7 +153,7 @@ public function testGetFormWithUserDefinedAttribute() ); self::assertContains( - '<option value="3" selected="selected">Customer Group 1</option>', + '<option value="3" selected="selected">Retailer</option>', $content, 'The Customer Group specified for the chosen store should be selected.' ); @@ -162,13 +199,13 @@ private function createCustomerGroupAttribute(): AttributeMetadataInterface { /** @var Option $option1 */ $option1 = $this->objectManager->create(Option::class); - $option1->setValue(3); - $option1->setLabel('Customer Group 1'); + $option1->setValue(2); + $option1->setLabel('Wholesale'); /** @var Option $option2 */ $option2 = $this->objectManager->create(Option::class); - $option2->setValue(4); - $option2->setLabel('Customer Group 2'); + $option2->setValue(3); + $option2->setLabel('Retailer'); /** @var AttributeMetadataInterfaceFactory $attributeMetadataFactory */ $attributeMetadataFactory = $this->objectManager->create(AttributeMetadataInterfaceFactory::class); diff --git a/dev/tests/integration/testsuite/Magento/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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Guest; + +use Magento\Customer\Model\Session; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * @magentoAppIsolation enabled + */ +class FormTest extends AbstractController +{ + /** + * Test view order as guest with correct data + * + * @magentoDataFixture Magento/Sales/_files/order.php + */ + public function testViewOrderAsGuest() + { + $this->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/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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Model\Service; + +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Tests \Magento\Sales\Model\Service\InvoiceService + */ +class InvoiceServiceTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var InvoiceService + */ + private $invoiceService; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->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/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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SendFriend\Controller; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Model\Session; +use Magento\Framework\Data\Form\FormKey; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Class SendmailTest + */ +class SendmailTest extends AbstractController +{ + /** + * Share the product to friend as logged in customer + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/SendFriend/_files/disable_allow_guest_config.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testSendActionAsLoggedIn() + { + $product = $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Config\Model\Config; +use Magento\Framework\App\Config\Storage\WriterInterface; + +$processConfigData = function (Config $config, array $data) { + foreach ($data as $key => $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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Config\Model\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/process_config_data.php'; + +$objectManager = Bootstrap::getObjectManager(); + +$configData = [ + 'sendfriend/email/max_per_hour' => 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/process_config_data.php'; + +$objectManager = Bootstrap::getObjectManager(); + +$configData = [ + 'sendfriend/email/max_per_hour', + 'sendfriend/email/check_by' +]; +/** @var WriterInterface $configWriter */ +$configWriter = $objectManager->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\App\Config\Value; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Value $config */ +$config = Bootstrap::getObjectManager()->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @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 = $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/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/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('<select name="block" id=""', $this->getResponse()->getBody()); } diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php index 92eae7a3fe3d7..940d05eb4d5d7 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php @@ -128,6 +128,7 @@ public function testAddActionProductNameXss() public function testAllcartAction() { $formKey = $this->_objectManager->get(\Magento\Framework\Data\Form\FormKey::class)->getFormKey(); + $this->getRequest()->setMethod('POST'); $this->getRequest()->setParam('form_key', $formKey); $this->dispatch('wishlist/index/allcart'); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js index 395dd96822ea5..c2a20c8339c7c 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js @@ -25,17 +25,7 @@ define([ 'Magento_Checkout/js/model/payment/additional-validators': { validate: jasmine.createSpy().and.returnValue(true) }, - 'Magento_Ui/js/model/messageList': { - addErrorMessage: jasmine.createSpy(), - addSuccessMessage: jasmine.createSpy() - }, - 'paypalInContextExpressCheckout': { - checkout: { - initXO: jasmine.createSpy(), - closeFlow: jasmine.createSpy(), - startFlow: jasmine.createSpy() - } - }, + 'Magento_Paypal/js/in-context/express-checkout-smart-buttons': jasmine.createSpy(), 'Magento_Customer/js/customer-data': { invalidate: jasmine.createSpy() } @@ -43,6 +33,15 @@ define([ obj; beforeAll(function (done) { + window.checkoutConfig = { + quoteData: { + entityId: 1 + }, + formKey: 'formKey' + }; + window.customerData = { + id: 1 + }; injector.mock(mocks); injector.require( ['Magento_Paypal/js/view/payment/method-renderer/in-context/checkout-express'], @@ -51,9 +50,11 @@ define([ provider: 'provName', name: 'test', index: 'test', - selectPaymentMethod: jasmine.createSpy() + item: { + method: 'payflow_express_bml' + }, + clientConfig: {} }); - done(); }); }); @@ -65,77 +66,11 @@ define([ } catch (e) {} }); - describe('"click" method checks', function () { - it('check success request', function () { - mocks['Magento_Paypal/js/action/set-payment-method'].and.callFake(function () { - var promise = $.Deferred(); - - promise.resolve(); - - return promise; - }); - spyOn(jQuery, 'get').and.callFake(function () { - var promise = $.Deferred(); - - promise.resolve({ - url: 'url' - }); - - return promise; - }); - - obj.clientConfig.click(new Event('event')); - - expect(mocks.paypalInContextExpressCheckout.checkout.initXO).toHaveBeenCalled(); - expect(mocks.paypalInContextExpressCheckout.checkout.startFlow).toHaveBeenCalledWith('url'); - expect(mocks.paypalInContextExpressCheckout.checkout.closeFlow).not.toHaveBeenCalled(); - expect(mocks['Magento_Customer/js/customer-data'].invalidate).toHaveBeenCalled(); - }); - - it('check request with error message', function () { - mocks['Magento_Paypal/js/action/set-payment-method'].and.callFake(function () { - var promise = $.Deferred(); - - promise.resolve(); - - return promise; - }); - spyOn(jQuery, 'get').and.callFake(function () { - var promise = $.Deferred(); - - promise.resolve({ - message: { - text: 'Text', - type: 'error' - } - }); - - return promise; - }); - - obj.clientConfig.click(new Event('event')); - - expect(mocks['Magento_Ui/js/model/messageList'].addErrorMessage).toHaveBeenCalledWith({ - message: 'Text' - }); - expect(mocks.paypalInContextExpressCheckout.checkout.initXO).toHaveBeenCalled(); - expect(mocks.paypalInContextExpressCheckout.checkout.closeFlow).toHaveBeenCalled(); - expect(mocks['Magento_Customer/js/customer-data'].invalidate).toHaveBeenCalled(); - }); - - it('check on fail request', function () { - mocks['Magento_Paypal/js/action/set-payment-method'].and.callFake(function () { - var promise = $.Deferred(); - - promise.reject(); - - return promise; - }); - spyOn(jQuery, 'get'); - - obj.clientConfig.click(new Event('event')); + describe('check smart button initialization', function () { + it('express-checkout-smart-buttons is initialized', function () { - expect(jQuery.get).not.toHaveBeenCalled(); + obj.renderPayPalButtons(); + expect(mocks['Magento_Paypal/js/in-context/express-checkout-smart-buttons']).toHaveBeenCalled(); }); }); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js index 9950c89ce3c9d..29a2e8db914a7 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js @@ -65,7 +65,7 @@ define([ name: 'test', index: 'test', item: { - method: 'paypal_express_bml' + method: 'payflow_express_bml' } }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/adminhtml/js/form/insert.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/adminhtml/js/form/insert.test.js new file mode 100644 index 0000000000000..dbe76f0dc2692 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/adminhtml/js/form/insert.test.js @@ -0,0 +1,45 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint-disable max-nested-callbacks */ +define([ + 'jquery', + 'Magento_Ui/js/form/components/insert' +], function ($, Insert) { + 'use strict'; + + describe('Magento_Ui/js/form/components/insert', function () { + var obj, params; + + beforeEach(function () { + params = { + isRendered: false, + autoRender: false + }; + obj = new Insert(params); + }); + + describe('"onRender" method', function () { + it('Check method call with not JSON response', function () { + var data = '<Not JSON>'; + + obj.onRender(data); + + expect(obj.content()).toBe(data); + expect(obj.isRendered).toBeTruthy(); + expect(obj.startRender).toBeFalsy(); + }); + + it('Check method call with ajaxExpired JSON', function () { + var data = '{"ajaxExpired": 1, "ajaxRedirect": "#test"}'; + + obj.onRender(data); + + expect(obj.content()).toBe(''); + expect(window.location.href).toContain('#test'); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/select.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/select.test.js index 057b22a752987..b36a075c9a777 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/select.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/select.test.js @@ -1,5 +1,5 @@ /** - * Copyright © 2016 Magento. All rights reserved. + * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ /*eslint max-nested-callbacks: 0*/ diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disabling_tables/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disabling_tables/db_schema.xml new file mode 100644 index 0000000000000..5ea5816b1df8e --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disabling_tables/db_schema.xml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="reference_table" resource="default"> + <column xsi:type="tinyint" name="tinyint_ref" padding="7" nullable="false" identity="true" unsigned="false"/> + <column xsi:type="tinyint" name="tinyint_without_padding" default="0" nullable="false" unsigned="false"/> + <column xsi:type="bigint" name="bigint_without_padding" default="0" nullable="false" unsigned="false"/> + <column xsi:type="smallint" name="smallint_without_padding" default="0" nullable="false" unsigned="false"/> + <column xsi:type="int" name="integer_without_padding" default="0" nullable="false" unsigned="false"/> + <column xsi:type="smallint" name="smallint_with_big_padding" padding="254" default="0" nullable="false" + unsigned="false"/> + <constraint xsi:type="primary" referenceId="tinyint_primary"> + <column name="tinyint_ref"/> + </constraint> + <index referenceId="COMPLEX_INDEX" indexType="btree"> + <column name="tinyint_without_padding"/> + <column name="bigint_without_padding"/> + </index> + </table> + <table name="auto_increment_test" resource="default"> + <column xsi:type="int" name="int_auto_increment_with_nullable" identity="true" padding="12" unsigned="true" + nullable="true"/> + <column xsi:type="smallint" name="int_disabled_auto_increment" default="0" identity="false" padding="12" + unsigned="true" nullable="true"/> + <constraint xsi:type="unique" referenceId="AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE"> + <column name="int_auto_increment_with_nullable"/> + </constraint> + </table> +</schema> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/InstallSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/InstallSchema.php new file mode 100644 index 0000000000000..c1eaff264df0c --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/InstallSchema.php @@ -0,0 +1,334 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestSetupDeclarationModule8\Setup; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Setup\InstallSchemaInterface; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; + +/** + * Install schema script for the TestSetupDeclarationModule8 module. + */ +class InstallSchema implements InstallSchemaInterface +{ + /** + * The name of the main table of Module8. + */ + const MAIN_TABLE = 'module8_test_main_table'; + + /** + * The name of the second table of Module8. + */ + const SECOND_TABLE = 'module8_test_second_table'; + + /** + * The name of the second table of Module8. + */ + const TEMP_TABLE = 'module8_test_install_temp_table'; + + /** + * @inheritdoc + * @throws \Zend_Db_Exception + */ + public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + + $this->createTables($setup); + + $setup->endSetup(); + } + + /** + * Create tables. + * + * @param SchemaSetupInterface $installer + * @throws \Zend_Db_Exception + */ + private function createTables(SchemaSetupInterface $installer) + { + $mainTableName = $installer->getTable(self::MAIN_TABLE); + $this->dropTableIfExists($installer, $mainTableName); + $mainTable = $installer->getConnection()->newTable($mainTableName); + $mainTable->setComment('Main Test Table for Module8'); + $this->addColumnsToMainTable($mainTable); + $this->addIndexesToMainTable($mainTable); + $installer->getConnection()->createTable($mainTable); + + $secondTableName = $installer->getTable(self::SECOND_TABLE); + $this->dropTableIfExists($installer, $secondTableName); + $secondTable = $installer->getConnection()->newTable($secondTableName); + $secondTable->setComment('Second Test Table for Module8'); + $this->addColumnsToSecondTable($secondTable); + $this->addIndexesToSecondTable($secondTable); + $this->addConstraintsToSecondTable($secondTable); + $installer->getConnection()->createTable($secondTable); + + $this->createSimpleTable($installer, self::TEMP_TABLE); + } + + /** + * Drop existing tables. + * + * @param SchemaSetupInterface $installer + * @param string $table + */ + private function dropTableIfExists($installer, $table) + { + $connection = $installer->getConnection(); + if ($connection->isTableExists($installer->getTable($table))) { + $connection->dropTable( + $installer->getTable($table) + ); + } + } + + /** + * Add tables to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumnsToMainTable($table) + { + $table + ->addColumn( + 'module8_email_contact_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'identity' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Email Contact ID' + )->addColumn( + 'module8_contact_group_id', + Table::TYPE_INTEGER, + 10, + [ + 'unsigned' => true, + 'nullable' => false + ], + 'Contact Group ID' + )->addColumn( + 'module8_is_guest', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Is Guest' + )->addColumn( + 'module8_contact_id', + Table::TYPE_TEXT, + 15, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Contact ID' + )->addColumn( + 'module8_content', + Table::TYPE_TEXT, + 15, + [ + 'nullable' => false, + ], + 'Content' + ); + } + + /** + * Add indexes to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexesToMainTable($table) + { + $table + ->addIndex( + 'MODULE8_INSTALL_INDEX_1', + ['module8_email_contact_id'] + )->addIndex( + 'MODULE8_INSTALL_UNIQUE_INDEX_2', + ['module8_email_contact_id', 'module8_is_guest'], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + )->addIndex( + 'MODULE8_INSTALL_INDEX_3', + ['module8_is_guest'] + )->addIndex( + 'MODULE8_INSTALL_INDEX_4', + ['module8_contact_id'] + )->addIndex( + 'MODULE8_INSTALL_INDEX_TEMP', + ['module8_content'] + )->addIndex( + 'MODULE8_INSTALL_UNIQUE_INDEX_TEMP', + ['module8_contact_group_id'], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + ); + } + + /** + * Add tables to second table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumnsToSecondTable($table) + { + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'identity' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Entity ID' + )->addColumn( + 'module8_contact_id', + Table::TYPE_INTEGER, + null, + [], + 'Contact ID' + )->addColumn( + 'module8_address', + Table::TYPE_TEXT, + 15, + [ + 'nullable' => false, + ], + 'Address' + )->addColumn( + 'module8_counter_with_multiline_comment', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true, + 'default' => 0 + ], + 'Empty + Counter + Multiline + Comment' + )->addColumn( + 'module8_second_address', + Table::TYPE_TEXT, + 15, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Second Address' + )->addColumn( + 'module8_temp_column', + Table::TYPE_TEXT, + 15, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Temp column for remove' + ); + } + + /** + * Add indexes to second table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexesToSecondTable($table) + { + $table + ->addIndex( + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_1', + ['module8_entity_id'] + )->addIndex( + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_2', + ['module8_address'] + )->addIndex( + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP', + ['module8_second_address'] + ); + } + + /** + * Add constraints to second table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addConstraintsToSecondTable($table) + { + $table + ->addForeignKey( + 'MODULE8_INSTALL_FK_ENTITY_ID_TEST_MAIN_TABLE_EMAIL_CONTACT_ID', + 'module8_entity_id', + self::MAIN_TABLE, + 'module8_email_contact_id' + )->addForeignKey( + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID', + 'module8_address', + self::MAIN_TABLE, + 'module8_contact_id' + )->addForeignKey( + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP', + 'module8_address', + self::MAIN_TABLE, + 'module8_content' + ); + } + + /** + * Create a simple table. + * + * @param SchemaSetupInterface $setup + * @param $tableName + * @throws \Zend_Db_Exception + */ + private function createSimpleTable(SchemaSetupInterface $setup, $tableName): void + { + $table = $setup->getConnection()->newTable($tableName); + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + null, + [ + 'primary' => true, + 'identity' => true, + 'nullable' => false, + 'unsigned' => true, + ], + 'Entity ID' + )->addColumn( + 'module8_counter', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true, + 'nullable' => true, + 'default' => 100 + ], + 'Counter' + ); + $setup->getConnection()->createTable($table); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/UpgradeSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/UpgradeSchema.php new file mode 100644 index 0000000000000..2dc8667a75dc1 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/UpgradeSchema.php @@ -0,0 +1,242 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestSetupDeclarationModule8\Setup; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; +use Magento\Framework\Setup\UpgradeSchemaInterface; + +/** + * Upgrade schema script for the TestSetupDeclarationModule8 module. + */ +class UpgradeSchema implements UpgradeSchemaInterface +{ + /** + * The name of the main table of the Module8. + */ + const UPDATE_TABLE = 'module8_test_update_table'; + + /** + * The name of the temporary table of the Module8. + */ + const TEMP_TABLE = 'module8_test_temp_table'; + + /** + * @inheritdoc + * @throws \Zend_Db_Exception + */ + public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + + if (version_compare($context->getVersion(), '1.0.1', '<')) { + $tableName = $setup->getTable(self::UPDATE_TABLE); + $table = $setup->getConnection()->newTable($tableName); + $table->setComment('Update Test Table for Module8'); + + $this->addColumns($setup, $table); + $this->addIndexes($table); + $this->addConstraints($table); + $setup->getConnection()->createTable($table); + + $this->createSimpleTable($setup, $setup->getTable(self::TEMP_TABLE)); + } + + if (version_compare($context->getVersion(), '1.0.2', '<')) { + $connection = $setup->getConnection(); + $connection + ->dropTable( + InstallSchema::TEMP_TABLE + ); + $connection + ->dropColumn( + InstallSchema::SECOND_TABLE, + 'module8_temp_column' + ); + $connection + ->dropForeignKey( + InstallSchema::SECOND_TABLE, + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP' + ); + $connection + ->dropIndex( + InstallSchema::MAIN_TABLE, + 'MODULE8_INSTALL_INDEX_TEMP' + ); + $connection + ->dropIndex( + InstallSchema::MAIN_TABLE, + 'MODULE8_INSTALL_UNIQUE_INDEX_TEMP' + ); + } + + $setup->endSetup(); + } + + /** + * Create columns for tables. + * + * @param SchemaSetupInterface $setup + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumns(SchemaSetupInterface $setup, Table $table): void + { + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Entity ID' + )->addColumn( + 'module8_entity_row_id', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true, + 'nullable' => false + ] + )->addColumn( + 'module8_is_guest', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Is Guest' + )->addColumn( + 'module8_guest_browser_id', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Guest Browser ID' + )->addColumn( + 'module8_column_for_remove', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'For remove' + ); + + $setup->getConnection()->addColumn( + InstallSchema::MAIN_TABLE, + 'module8_update_column', + [ + 'type' => Table::TYPE_INTEGER, + 'nullable' => false, + 'comment' => 'Module_8 Update Column', + ] + ); + } + + /** + * Add indexes. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexes(Table $table): void + { + $table + ->addIndex( + 'MODULE8_UPDATE_IS_GUEST_INDEX', + [ + 'module8_is_guest' + ] + )->addIndex( + 'MODULE8_UPDATE_UNIQUE_INDEX_TEMP', + [ + 'module8_entity_id', + 'module8_is_guest', + + ], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + )->addIndex( + 'MODULE8_UPDATE_TEMP_INDEX', + [ + 'module8_column_for_remove', + 'module8_guest_browser_id' + ] + ); + } + + /** + * Add constraints. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addConstraints(Table $table): void + { + $table + ->addForeignKey( + 'MODULE8_UPDATE_FK_MODULE8_IS_GUEST', + 'module8_is_guest', + InstallSchema::MAIN_TABLE, + 'module8_is_guest', + Table::ACTION_CASCADE + )->addForeignKey( + 'MODULE8_UPDATE_FK_TEMP', + 'module8_column_for_remove', + InstallSchema::MAIN_TABLE, + 'module8_is_guest', + Table::ACTION_CASCADE + ); + } + + /** + * Create a simple table. + * + * @param SchemaSetupInterface $setup + * @param $tableName + * @throws \Zend_Db_Exception + */ + private function createSimpleTable(SchemaSetupInterface $setup, $tableName): void + { + $table = $setup->getConnection()->newTable($tableName); + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + null, + [ + 'primary' => true, + 'identity' => true, + 'nullable' => false, + 'unsigned' => true, + ], + 'Entity ID' + )->addColumn( + 'module8_counter', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true, + 'nullable' => true, + 'default' => 100 + ], + 'Counter' + ); + $setup->getConnection()->createTable($table); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/module.xml new file mode 100644 index 0000000000000..b6a57fcb47639 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/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_TestSetupDeclarationModule8" setup_version="1.0.2"/> +</config> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/etc/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/etc/module.xml new file mode 100644 index 0000000000000..e472f951b08e9 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/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_TestSetupDeclarationModule9" setup_version="1.0.0" /> +</config> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php new file mode 100644 index 0000000000000..633185390ae84 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(12) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(12) unsigned DEFAULT \'0\', + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/registration.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/registration.php new file mode 100644 index 0000000000000..f021af556bb73 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestSetupDeclarationModule9') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestSetupDeclarationModule9', __DIR__); +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/disabling_tables/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/disabling_tables/db_schema.xml new file mode 100644 index 0000000000000..c22c41b7a5c03 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/disabling_tables/db_schema.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="reference_table" disabled="true"/> +</schema> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/InstallSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/InstallSchema.php new file mode 100644 index 0000000000000..d78a8e25230b4 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/InstallSchema.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestSetupDeclarationModule9\Setup; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Setup\InstallSchemaInterface; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; + +/** + * Install schema script for the TestSetupDeclarationModule9 module. + */ +class InstallSchema implements InstallSchemaInterface +{ + /** + * The name of the main table of Module9. + */ + const MAIN_TABLE = 'module9_test_main_table'; + + /** + * @inheritdoc + * @throws \Zend_Db_Exception + */ + public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + + $this->createTables($setup); + + $setup->endSetup(); + } + + /** + * Create tables. + * + * @param SchemaSetupInterface $installer + * @throws \Zend_Db_Exception + */ + private function createTables(SchemaSetupInterface $installer) + { + $mainTableName = $installer->getTable(self::MAIN_TABLE); + $this->dropTableIfExists($installer, $mainTableName); + $mainTable = $installer->getConnection()->newTable($mainTableName); + $mainTable->setComment('Main Test Table for Module9'); + $this->addColumnsToMainTable($mainTable); + $this->addIndexesToMainTable($mainTable); + $installer->getConnection()->createTable($mainTable); + } + + /** + * Drop existing tables. + * + * @param SchemaSetupInterface $installer + * @param string $table + */ + private function dropTableIfExists($installer, $table) + { + $connection = $installer->getConnection(); + if ($connection->isTableExists($installer->getTable($table))) { + $connection->dropTable( + $installer->getTable($table) + ); + } + } + + /** + * Add tables to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumnsToMainTable($table) + { + $table + ->addColumn( + 'module9_email_contact_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'identity' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Entity ID' + )->addColumn( + 'module9_is_guest', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Is Guest' + )->addColumn( + 'module9_guest_id', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true + ], + 'Guest ID' + ) + ->addColumn( + 'module9_created_at', + Table::TYPE_DATE, + null, + [], + 'Created At' + ); + } + + /** + * Add indexes to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexesToMainTable($table) + { + $table + ->addIndex( + 'MODULE9_INSTALL_UNIQUE_INDEX_1', + ['module9_email_contact_id', 'module9_guest_id'], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + ); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/UpgradeSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/UpgradeSchema.php new file mode 100644 index 0000000000000..0a49349699963 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/UpgradeSchema.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestSetupDeclarationModule9\Setup; + +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; +use Magento\Framework\Setup\UpgradeSchemaInterface; +use Magento\TestSetupDeclarationModule8\Setup\InstallSchema as Module8InstallSchema; +use Magento\TestSetupDeclarationModule8\Setup\UpgradeSchema as Module8UpgradeSchema; + +/** + * Upgrade schema script for the TestSetupDeclarationModule9 module. + */ +class UpgradeSchema implements UpgradeSchemaInterface +{ + /** + * The name of the main table of Module9. + */ + const REPLICA_TABLE = 'module9_test_update_replica_table'; + + /** + * @inheritdoc + * @throws \Zend_Db_Exception + */ + public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + + if (version_compare($context->getVersion(), '2.0.0', '<')) { + $this->addColumns($setup); + $this->addIndexes($setup); + $this->addConstraints($setup); + $this->removeColumns($setup); + $this->removeIndexes($setup); + $this->removeConstraints($setup); + $this->removeTables($setup); + $replicaTable = $setup->getConnection() + ->createTableByDdl(Module8InstallSchema::SECOND_TABLE, self::REPLICA_TABLE); + $setup->getConnection()->createTable($replicaTable); + } + + $setup->endSetup(); + } + + /** + * Create columns for tables. + * + * @param SchemaSetupInterface $setup + */ + private function addColumns(SchemaSetupInterface $setup): void + { + $setup->getConnection()->addColumn( + InstallSchema::MAIN_TABLE, + 'module9_update_column', + [ + 'type' => Table::TYPE_INTEGER, + 'nullable' => false, + 'comment' => 'Module_9 Update Column', + ] + ); + + $setup->getConnection()->addColumn( + Module8InstallSchema::MAIN_TABLE, + 'module9_update_column', + [ + 'type' => Table::TYPE_INTEGER, + 'nullable' => false, + 'comment' => 'Module_9 Update Column', + ] + ); + } + + /** + * Add indexes. + * + * @param SchemaSetupInterface $setup + */ + private function addIndexes(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->addIndex( + Module8UpgradeSchema::UPDATE_TABLE, + 'MODULE9_UPDATE_MODULE8_GUEST_BROWSER_ID', + [ + 'module8_guest_browser_id' + ] + ); + } + + /** + * Add constraints. + * + * @param SchemaSetupInterface $setup + */ + private function addConstraints(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->addForeignKey( + 'MODULE9_UPDATE_FK_MODULE9_IS_GUEST', + InstallSchema::MAIN_TABLE, + 'module9_is_guest', + Module8InstallSchema::MAIN_TABLE, + 'module8_is_guest', + Table::ACTION_CASCADE + ); + } + + /** + * Remove columns. + * + * @param SchemaSetupInterface $setup + */ + private function removeColumns(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->dropColumn( + Module8UpgradeSchema::UPDATE_TABLE, + 'module8_column_for_remove' + ); + } + + /** + * Remove indexes. + * + * @param SchemaSetupInterface $setup + */ + private function removeIndexes(SchemaSetupInterface $setup): void + { + $connection = $setup->getConnection(); + $connection + ->dropIndex( + Module8InstallSchema::SECOND_TABLE, + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP' + ); + } + + /** + * Remove constraints. + * + * @param SchemaSetupInterface $setup + */ + private function removeConstraints(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->dropForeignKey( + Module8InstallSchema::SECOND_TABLE, + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID' + )->dropIndex( + Module8UpgradeSchema::UPDATE_TABLE, + 'MODULE8_UPDATE_UNIQUE_INDEX_TEMP' + ); + } + + /** + * Remove tables. + * + * @param SchemaSetupInterface $setup + */ + private function removeTables(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->dropTable( + Module8UpgradeSchema::TEMP_TABLE + ); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/module.xml new file mode 100644 index 0000000000000..a553b82d78148 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/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_TestSetupDeclarationModule9" setup_version="2.0.0"> + <sequence> + <module name="Magento_TestSetupDeclarationModule8"/> + </sequence> + </module> +</config> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupInstallTest.php b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupInstallTest.php new file mode 100644 index 0000000000000..cf137233ead0f --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupInstallTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Developer\Console\Command; + +use Magento\Framework\Component\ComponentRegistrar; +use Magento\TestFramework\Deploy\CliCommand; +use Magento\TestFramework\Deploy\TestModuleManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\SetupTestCase; + +/** + * Test for Install command. + */ +class SetupInstallTest extends SetupTestCase +{ + /** + * @var TestModuleManager + */ + private $moduleManager; + + /** + * @var CliCommand + */ + private $cliCommand; + + /** + * @var ComponentRegistrar + */ + private $componentRegistrar; + + /** + * @inheritdoc + */ + public function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->cliCommand = $objectManager->get(CliCommand::class); + $this->moduleManager = $objectManager->get(TestModuleManager::class); + $this->componentRegistrar = $objectManager->create( + ComponentRegistrar::class + ); + } + + /** + * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule9 + * @throws \Exception + */ + public function testInstallWithConverting() + { + $modules = [ + 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule9', + ]; + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'InstallSchema.php', + 'Setup' + ); + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'UpgradeSchema.php', + 'Setup' + ); + + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'module.xml', + 'etc' + ); + } + + $this->cliCommand->install($modules, ['convert-old-scripts' => true]); + + foreach ($modules as $moduleName) { + $modulePath = $this->componentRegistrar->getPath('module', $moduleName); + $schemaFileName = $modulePath + . DIRECTORY_SEPARATOR + . \Magento\Framework\Module\Dir::MODULE_ETC_DIR + . DIRECTORY_SEPARATOR + . 'db_schema.xml'; + $generatedSchema = $this->getSchemaDocument($schemaFileName); + + $expectedSchemaFileName = dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + 'SetupInstall', + str_replace('Magento_', '', $moduleName), + 'db_schema.xml' + ] + ); + $expectedSchema = $this->getSchemaDocument($expectedSchemaFileName); + + $this->assertEquals($expectedSchema->saveXML(), $generatedSchema->saveXML()); + } + } + + /** + * Convert file content in the DOM document. + * + * @param $schemaFileName + * @return \DOMDocument + */ + private function getSchemaDocument($schemaFileName): \DOMDocument + { + $schemaDocument = new \DOMDocument(); + $schemaDocument->preserveWhiteSpace = false; + $schemaDocument->formatOutput = true; + $schemaDocument->loadXML(file_get_contents($schemaFileName)); + + return $schemaDocument; + } +} diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupUpgradeTest.php b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupUpgradeTest.php new file mode 100644 index 0000000000000..932662b58f3ac --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupUpgradeTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Developer\Console\Command; + +use Magento\Framework\Component\ComponentRegistrar; +use Magento\TestFramework\Deploy\CliCommand; +use Magento\TestFramework\Deploy\TestModuleManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\SetupTestCase; + +/** + * Test for Upgrade command. + */ +class SetupUpgradeTest extends SetupTestCase +{ + /** + * @var TestModuleManager + */ + private $moduleManager; + + /** + * @var CliCommand + */ + private $cliCommand; + + /** + * @var ComponentRegistrar + */ + private $componentRegistrar; + + /** + * @inheritdoc + */ + public function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->cliCommand = $objectManager->get(CliCommand::class); + $this->moduleManager = $objectManager->get(TestModuleManager::class); + $this->componentRegistrar = $objectManager->create( + ComponentRegistrar::class + ); + } + + /** + * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule9 + * @throws \Exception + */ + public function testUpgradeWithConverting() + { + $modules = [ + 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule9', + ]; + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'InstallSchema.php', + 'Setup' + ); + } + + $this->cliCommand->install($modules, ['convert-old-scripts' => true]); + foreach ($modules as $moduleName) { + $this->assertInstallScriptChanges($moduleName); + } + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'UpgradeSchema.php', + 'Setup' + ); + + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'module.xml', + 'etc' + ); + } + + $this->cliCommand->upgrade(['convert-old-scripts' => true]); + + foreach ($modules as $moduleName) { + $this->assertUpgradeScriptChanges($moduleName); + } + } + + /** + * Convert file content in the DOM document. + * + * @param string $schemaFileName + * @return \DOMDocument + */ + private function getSchemaDocument(string $schemaFileName): \DOMDocument + { + $schemaDocument = new \DOMDocument(); + $schemaDocument->preserveWhiteSpace = false; + $schemaDocument->formatOutput = true; + $schemaDocument->loadXML(file_get_contents($schemaFileName)); + + return $schemaDocument; + } + + /** + * @param string $moduleName + */ + private function assertInstallScriptChanges(string $moduleName): void + { + $generatedSchema = $this->getGeneratedSchema($moduleName); + $expectedSchema = $this->getSchemaDocument($this->getSchemaFixturePath($moduleName, 'install')); + + $this->assertEquals($expectedSchema->saveXML(), $generatedSchema->saveXML()); + } + + /** + * @param string $moduleName + */ + private function assertUpgradeScriptChanges(string $moduleName): void + { + $generatedSchema = $this->getGeneratedSchema($moduleName); + $expectedSchema = $this->getSchemaDocument($this->getSchemaFixturePath($moduleName, 'upgrade')); + + $this->assertEquals($expectedSchema->saveXML(), $generatedSchema->saveXML()); + } + + /** + * @param string $moduleName + * @return \DOMDocument + */ + private function getGeneratedSchema(string $moduleName): \DOMDocument + { + $modulePath = $this->componentRegistrar->getPath('module', $moduleName); + $schemaFileName = $modulePath + . DIRECTORY_SEPARATOR + . \Magento\Framework\Module\Dir::MODULE_ETC_DIR + . DIRECTORY_SEPARATOR + . 'db_schema.xml'; + + return $this->getSchemaDocument($schemaFileName); + } + + /** + * @param string $moduleName + * @param string $suffix + * @return string + */ + private function getSchemaFixturePath(string $moduleName, string $suffix): string + { + $schemaFixturePath = dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + 'SetupUpgrade', + str_replace('Magento_', '', $moduleName), + 'db_schema_' . $suffix . '.xml' + ] + ); + + return $schemaFixturePath; + } +} diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule8/db_schema.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule8/db_schema.xml new file mode 100644 index 0000000000000..cdc71980bf50d --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule8/db_schema.xml @@ -0,0 +1,114 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module8_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module8"> + <column xsi:type="int" name="module8_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Email Contact ID"/> + <column xsi:type="int" name="module8_contact_group_id" padding="10" unsigned="true" nullable="false" + identity="false" comment="Contact Group ID"/> + <column xsi:type="smallint" name="module8_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="varchar" name="module8_contact_id" nullable="true" length="15" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_content" nullable="false" length="15" comment="Content"/> + <column xsi:type="int" name="module8_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_8 Update Column"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_2"> + <column name="module8_email_contact_id"/> + <column name="module8_is_guest"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_TEMP" disabled="true"> + <column name="module8_contact_group_id"/> + </constraint> + <index referenceId="MODULE8_INSTALL_INDEX_1" indexType="btree"> + <column name="module8_email_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_3" indexType="btree"> + <column name="module8_is_guest"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_4" indexType="btree"> + <column name="module8_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_TEMP" indexType="btree" disabled="true"> + <column name="module8_content"/> + </index> + </table> + <table name="module8_test_second_table" resource="default" engine="innodb" comment="Second Test Table for Module8"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_contact_id" padding="11" unsigned="false" nullable="true" + identity="false" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_address" nullable="false" length="15" comment="Address"/> + <column xsi:type="smallint" name="module8_counter_with_multiline_comment" padding="5" unsigned="true" + nullable="true" identity="false" default="0" + comment="Empty Counter Multiline Comment"/> + <column xsi:type="varchar" name="module8_second_address" nullable="true" length="15" comment="Second Address"/> + <column xsi:type="varchar" name="module8_temp_column" nullable="true" length="15" + comment="Temp column for remove" disabled="true"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ENTITY_ID_TEST_MAIN_TABLE_EMAIL_CONTACT_ID" + table="module8_test_second_table" column="module8_entity_id" + referenceTable="module8_test_main_table" referenceColumn="module8_email_contact_id" + onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_contact_id" onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_content" onDelete="NO ACTION" disabled="true"/> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_1" indexType="btree"> + <column name="module8_entity_id"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_2" indexType="btree"> + <column name="module8_address"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP" indexType="btree"> + <column name="module8_second_address"/> + </index> + </table> + <table name="module8_test_update_table" resource="default" engine="innodb" comment="Update Test Table for Module8"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="Entity ID"/> + <column xsi:type="int" name="module8_entity_row_id" padding="10" unsigned="true" nullable="false" + identity="false" comment="Module8_entity_row_id"/> + <column xsi:type="smallint" name="module8_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="smallint" name="module8_guest_browser_id" padding="5" unsigned="true" nullable="true" + identity="false" comment="Guest Browser ID"/> + <column xsi:type="smallint" name="module8_column_for_remove" padding="5" unsigned="true" nullable="true" + identity="false" comment="For remove"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_MODULE8_IS_GUEST" + table="module8_test_update_table" column="module8_is_guest" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_TEMP" table="module8_test_update_table" + column="module8_column_for_remove" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + <constraint xsi:type="unique" referenceId="MODULE8_UPDATE_UNIQUE_INDEX_TEMP"> + <column name="module8_entity_id"/> + <column name="module8_is_guest"/> + </constraint> + <index referenceId="MODULE8_UPDATE_IS_GUEST_INDEX" indexType="btree"> + <column name="module8_is_guest"/> + </index> + <index referenceId="MODULE8_UPDATE_TEMP_INDEX" indexType="btree"> + <column name="module8_column_for_remove"/> + <column name="module8_guest_browser_id"/> + </index> + </table> + <table name="module8_test_temp_table" resource="default" engine="innodb" comment="module8_test_temp_table"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_counter" padding="10" unsigned="true" nullable="true" identity="false" + default="100" comment="Counter"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule9/db_schema.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule9/db_schema.xml new file mode 100644 index 0000000000000..3ded03c9e79f0 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule9/db_schema.xml @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module9_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module9"> + <column xsi:type="int" name="module9_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Entity ID"/> + <column xsi:type="smallint" name="module9_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="int" name="module9_guest_id" padding="10" unsigned="true" nullable="true" identity="false" + comment="Guest ID"/> + <column xsi:type="date" name="module9_created_at" comment="Created At"/> + <column xsi:type="int" name="module9_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_9 Update Column"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module9_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE9_INSTALL_UNIQUE_INDEX_1"> + <column name="module9_email_contact_id"/> + <column name="module9_guest_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE9_UPDATE_FK_MODULE9_IS_GUEST" table="module9_test_main_table" + column="module9_is_guest" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + </table> + <table name="module8_test_main_table" resource="default"> + <column xsi:type="int" name="module9_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_9 Update Column"/> + </table> + <table name="module8_test_update_table" resource="default"> + <column name="module8_column_for_remove" disabled="true"/> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_TEMP" disabled="true"/> + <index referenceId="MODULE9_UPDATE_MODULE8_GUEST_BROWSER_ID" indexType="btree"> + <column name="module8_guest_browser_id"/> + </index> + <index referenceId="MODULE8_UPDATE_UNIQUE_INDEX_TEMP" disabled="true"/> + </table> + <table name="module8_test_second_table" resource="default"> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID" + disabled="true"/> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP" disabled="true"/> + </table> + <table name="module8_test_temp_table" disabled="true" resource="default"/> + <table name="module9_test_update_replica_table" resource="default" engine="innodb" + comment="Module9 Test Update Replica Table"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Module8 Entity Id"/> + <column xsi:type="int" name="module8_contact_id" padding="11" unsigned="false" nullable="true" + identity="false" comment="Module8 Contact Id"/> + <column xsi:type="varchar" name="module8_address" nullable="false" length="15" comment="Module8 Address"/> + <column xsi:type="smallint" name="module8_counter_with_multiline_comment" padding="5" unsigned="true" + nullable="true" identity="false" default="0" comment="Module8 Counter With Multiline Comment"/> + <column xsi:type="varchar" name="module8_second_address" nullable="true" length="15" + comment="Module8 Second Address"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="FK_8914AF398964FAFB4ED2E382866ABBF4" + table="module9_test_update_replica_table" column="module8_entity_id" + referenceTable="module8_test_main_table" referenceColumn="module8_email_contact_id" + onDelete="NO ACTION"/> + <index referenceId="MODULE9_TEST_UPDATE_REPLICA_TABLE_MODULE8_ENTITY_ID" indexType="btree"> + <column name="module8_entity_id"/> + </index> + <index referenceId="MODULE9_TEST_UPDATE_REPLICA_TABLE_MODULE8_ADDRESS" indexType="btree"> + <column name="module8_address"/> + </index> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_install.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_install.xml new file mode 100644 index 0000000000000..2da9901cf9629 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_install.xml @@ -0,0 +1,81 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module8_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module8"> + <column xsi:type="int" name="module8_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Email Contact ID"/> + <column xsi:type="int" name="module8_contact_group_id" padding="10" unsigned="true" nullable="false" + identity="false" comment="Contact Group ID"/> + <column xsi:type="smallint" name="module8_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="varchar" name="module8_contact_id" nullable="true" length="15" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_content" nullable="false" length="15" comment="Content"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_2"> + <column name="module8_email_contact_id"/> + <column name="module8_is_guest"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_TEMP"> + <column name="module8_contact_group_id"/> + </constraint> + <index referenceId="MODULE8_INSTALL_INDEX_1" indexType="btree"> + <column name="module8_email_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_3" indexType="btree"> + <column name="module8_is_guest"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_4" indexType="btree"> + <column name="module8_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_TEMP" indexType="btree"> + <column name="module8_content"/> + </index> + </table> + <table name="module8_test_second_table" resource="default" engine="innodb" comment="Second Test Table for Module8"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_contact_id" padding="11" unsigned="false" nullable="true" + identity="false" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_address" nullable="false" length="15" comment="Address"/> + <column xsi:type="smallint" name="module8_counter_with_multiline_comment" padding="5" unsigned="true" + nullable="true" identity="false" default="0" + comment="Empty Counter Multiline Comment"/> + <column xsi:type="varchar" name="module8_second_address" nullable="true" length="15" comment="Second Address"/> + <column xsi:type="varchar" name="module8_temp_column" nullable="true" length="15" + comment="Temp column for remove"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ENTITY_ID_TEST_MAIN_TABLE_EMAIL_CONTACT_ID" + table="module8_test_second_table" column="module8_entity_id" + referenceTable="module8_test_main_table" referenceColumn="module8_email_contact_id" + onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_contact_id" onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_content" onDelete="NO ACTION"/> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_1" indexType="btree"> + <column name="module8_entity_id"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_2" indexType="btree"> + <column name="module8_address"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP" indexType="btree"> + <column name="module8_second_address"/> + </index> + </table> + <table name="module8_test_install_temp_table" resource="default" engine="innodb" + comment="module8_test_install_temp_table"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_counter" padding="10" unsigned="true" nullable="true" identity="false" + default="100" comment="Counter"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_upgrade.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_upgrade.xml new file mode 100644 index 0000000000000..6deed3105f292 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_upgrade.xml @@ -0,0 +1,124 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module8_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module8"> + <column xsi:type="int" name="module8_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Email Contact ID"/> + <column xsi:type="int" name="module8_contact_group_id" padding="10" unsigned="true" nullable="false" + identity="false" comment="Contact Group ID"/> + <column xsi:type="smallint" name="module8_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="varchar" name="module8_contact_id" nullable="true" length="15" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_content" nullable="false" length="15" comment="Content"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_2"> + <column name="module8_email_contact_id"/> + <column name="module8_is_guest"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_TEMP" disabled="true"> + <column name="module8_contact_group_id"/> + </constraint> + <index referenceId="MODULE8_INSTALL_INDEX_1" indexType="btree"> + <column name="module8_email_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_3" indexType="btree"> + <column name="module8_is_guest"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_4" indexType="btree"> + <column name="module8_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_TEMP" indexType="btree" disabled="true"> + <column name="module8_content"/> + </index> + <column xsi:type="int" name="module8_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_8 Update Column"/> + </table> + <table name="module8_test_second_table" resource="default" engine="innodb" comment="Second Test Table for Module8"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_contact_id" padding="11" unsigned="false" nullable="true" + identity="false" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_address" nullable="false" length="15" comment="Address"/> + <column xsi:type="smallint" name="module8_counter_with_multiline_comment" padding="5" unsigned="true" + nullable="true" identity="false" default="0" + comment="Empty Counter Multiline Comment"/> + <column xsi:type="varchar" name="module8_second_address" nullable="true" length="15" comment="Second Address"/> + <column xsi:type="varchar" name="module8_temp_column" nullable="true" length="15" + comment="Temp column for remove" disabled="true"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ENTITY_ID_TEST_MAIN_TABLE_EMAIL_CONTACT_ID" + table="module8_test_second_table" column="module8_entity_id" + referenceTable="module8_test_main_table" referenceColumn="module8_email_contact_id" + onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_contact_id" onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_content" onDelete="NO ACTION" disabled="true"/> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_1" indexType="btree"> + <column name="module8_entity_id"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_2" indexType="btree"> + <column name="module8_address"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP" indexType="btree"> + <column name="module8_second_address"/> + </index> + </table> + <table name="module8_test_install_temp_table" resource="default" engine="innodb" + comment="module8_test_install_temp_table" disabled="true"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_counter" padding="10" unsigned="true" nullable="true" identity="false" + default="100" comment="Counter"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + </table> + <table name="module8_test_update_table" resource="default" engine="innodb" comment="Update Test Table for Module8"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="Entity ID"/> + <column xsi:type="int" name="module8_entity_row_id" padding="10" unsigned="true" nullable="false" + identity="false" comment="Module8_entity_row_id"/> + <column xsi:type="smallint" name="module8_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="smallint" name="module8_guest_browser_id" padding="5" unsigned="true" nullable="true" + identity="false" comment="Guest Browser ID"/> + <column xsi:type="smallint" name="module8_column_for_remove" padding="5" unsigned="true" nullable="true" + identity="false" comment="For remove"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_MODULE8_IS_GUEST" + table="module8_test_update_table" column="module8_is_guest" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_TEMP" table="module8_test_update_table" + column="module8_column_for_remove" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + <constraint xsi:type="unique" referenceId="MODULE8_UPDATE_UNIQUE_INDEX_TEMP"> + <column name="module8_entity_id"/> + <column name="module8_is_guest"/> + </constraint> + <index referenceId="MODULE8_UPDATE_IS_GUEST_INDEX" indexType="btree"> + <column name="module8_is_guest"/> + </index> + <index referenceId="MODULE8_UPDATE_TEMP_INDEX" indexType="btree"> + <column name="module8_column_for_remove"/> + <column name="module8_guest_browser_id"/> + </index> + </table> + <table name="module8_test_temp_table" resource="default" engine="innodb" comment="module8_test_temp_table"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_counter" padding="10" unsigned="true" nullable="true" identity="false" + default="100" comment="Counter"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_install.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_install.xml new file mode 100644 index 0000000000000..2ac2cc607f0df --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_install.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module9_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module9"> + <column xsi:type="int" name="module9_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Entity ID"/> + <column xsi:type="smallint" name="module9_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="int" name="module9_guest_id" padding="10" unsigned="true" nullable="true" identity="false" + comment="Guest ID"/> + <column xsi:type="date" name="module9_created_at" comment="Created At"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module9_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE9_INSTALL_UNIQUE_INDEX_1"> + <column name="module9_email_contact_id"/> + <column name="module9_guest_id"/> + </constraint> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_upgrade.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_upgrade.xml new file mode 100644 index 0000000000000..b522224ca07b2 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_upgrade.xml @@ -0,0 +1,77 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module9_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module9"> + <column xsi:type="int" name="module9_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Entity ID"/> + <column xsi:type="smallint" name="module9_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="int" name="module9_guest_id" padding="10" unsigned="true" nullable="true" identity="false" + comment="Guest ID"/> + <column xsi:type="date" name="module9_created_at" comment="Created At"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module9_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE9_INSTALL_UNIQUE_INDEX_1"> + <column name="module9_email_contact_id"/> + <column name="module9_guest_id"/> + </constraint> + <column xsi:type="int" name="module9_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_9 Update Column"/> + <constraint xsi:type="foreign" referenceId="MODULE9_UPDATE_FK_MODULE9_IS_GUEST" table="module9_test_main_table" + column="module9_is_guest" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + </table> + <table name="module8_test_main_table" resource="default"> + <column xsi:type="int" name="module9_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_9 Update Column"/> + </table> + <table name="module8_test_update_table" resource="default"> + <column name="module8_column_for_remove" disabled="true"/> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_TEMP" disabled="true"/> + <index referenceId="MODULE9_UPDATE_MODULE8_GUEST_BROWSER_ID" indexType="btree"> + <column name="module8_guest_browser_id"/> + </index> + <index referenceId="MODULE8_UPDATE_UNIQUE_INDEX_TEMP" disabled="true"/> + </table> + <table name="module8_test_second_table" resource="default"> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID" + disabled="true"/> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP" disabled="true"/> + </table> + <table name="module8_test_temp_table" disabled="true" resource="default"/> + <table name="module9_test_update_replica_table" resource="default" engine="innodb" + comment="Module9 Test Update Replica Table"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Module8 Entity Id"/> + <column xsi:type="int" name="module8_contact_id" padding="11" unsigned="false" nullable="true" + identity="false" comment="Module8 Contact Id"/> + <column xsi:type="varchar" name="module8_address" nullable="false" length="15" comment="Module8 Address"/> + <column xsi:type="smallint" name="module8_counter_with_multiline_comment" padding="5" unsigned="true" + nullable="true" identity="false" default="0" comment="Module8 Counter With Multiline Comment"/> + <column xsi:type="varchar" name="module8_second_address" nullable="true" length="15" + comment="Module8 Second Address"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="FK_F205D8789B56A8E75BBFC0C68C041E98" + table="module9_test_update_replica_table" column="module8_address" + referenceTable="module8_test_main_table" referenceColumn="module8_content" onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="FK_C7075560727757663A51EC925F4032C9" + table="module9_test_update_replica_table" column="module8_address" + referenceTable="module8_test_main_table" referenceColumn="module8_contact_id" onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="FK_8914AF398964FAFB4ED2E382866ABBF4" + table="module9_test_update_replica_table" column="module8_entity_id" + referenceTable="module8_test_main_table" referenceColumn="module8_email_contact_id" + onDelete="NO ACTION"/> + <index referenceId="MODULE9_TEST_UPDATE_REPLICA_TABLE_MODULE8_ENTITY_ID" indexType="btree"> + <column name="module8_entity_id"/> + </index> + <index referenceId="MODULE9_TEST_UPDATE_REPLICA_TABLE_MODULE8_ADDRESS" indexType="btree"> + <column name="module8_address"/> + </index> + <index referenceId="MODULE9_TEST_UPDATE_REPLICA_TABLE_MODULE8_SECOND_ADDRESS" indexType="btree"> + <column name="module8_second_address"/> + </index> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php b/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php index f6497e8e4b162..6097348d4fabc 100644 --- a/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php +++ b/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php @@ -29,7 +29,7 @@ class DeclarativeInstallerTest extends SetupTestCase /** * @var CliCommand */ - private $cliCommad; + private $cliCommand; /** * @var SchemaDiff @@ -51,11 +51,14 @@ class DeclarativeInstallerTest extends SetupTestCase */ private $describeTable; + /** + * @inheritdoc + */ public function setUp() { $objectManager = Bootstrap::getObjectManager(); $this->moduleManager = $objectManager->get(TestModuleManager::class); - $this->cliCommad = $objectManager->get(CliCommand::class); + $this->cliCommand = $objectManager->get(CliCommand::class); $this->describeTable = $objectManager->get(DescribeTable::class); $this->schemaDiff = $objectManager->get(SchemaDiff::class); $this->schemaConfig = $objectManager->get(SchemaConfigInterface::class); @@ -68,7 +71,7 @@ public function setUp() */ public function testInstallation() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -104,10 +107,11 @@ private function compareStructures() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/column_modification.php + * @throws \Exception */ public function testInstallationWithColumnsModification() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -119,7 +123,7 @@ public function testInstallationWithColumnsModification() 'etc' ); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -157,14 +161,15 @@ private function updateDbSchemaRevision($revisionName) /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/column_removal.php + * @throws \Exception */ public function testInstallationWithColumnsRemoval() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $this->updateDbSchemaRevision('column_removals'); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -195,14 +200,15 @@ private function getTrimmedData() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.php + * @throws \Exception */ public function testInstallationWithConstraintsModification() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $this->updateDbSchemaRevision('constraint_modifications'); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $diff = $this->schemaDiff->diff( $this->schemaConfig->getDeclarationConfig(), @@ -216,10 +222,11 @@ public function testInstallationWithConstraintsModification() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.php + * @throws \Exception */ public function testInstallationWithDroppingTables() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -231,7 +238,7 @@ public function testInstallationWithDroppingTables() 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $diff = $this->schemaDiff->diff( $this->schemaConfig->getDeclarationConfig(), @@ -245,6 +252,7 @@ public function testInstallationWithDroppingTables() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.php + * @throws \Exception */ public function testInstallWithCodeBaseRollback() { @@ -255,7 +263,7 @@ public function testInstallWithCodeBaseRollback() 'db_schema.xml', 'etc' ); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $beforeRollback = $this->describeTable->describeShard('default'); @@ -268,7 +276,7 @@ public function testInstallWithCodeBaseRollback() 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $afterRollback = $this->describeTable->describeShard('default'); self::assertEquals($this->getData()['after'], $afterRollback); } @@ -276,6 +284,7 @@ public function testInstallWithCodeBaseRollback() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.php + * @throws \Exception */ public function testTableRename() { @@ -287,7 +296,7 @@ public function testTableRename() 'db_schema.xml', 'etc' ); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $before = $this->describeTable->describeShard('default'); @@ -305,7 +314,7 @@ public function testTableRename() 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $after = $this->describeTable->describeShard('default'); self::assertEquals($this->getData()['after'], $after['some_table_renamed']); $select = $adapter->select() @@ -315,10 +324,11 @@ public function testTableRename() /** * @moduleName Magento_TestSetupDeclarationModule8 + * @throws \Exception */ public function testForeignKeyReferenceId() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule8'] ); $this->moduleManager->updateRevision( @@ -327,7 +337,7 @@ public function testForeignKeyReferenceId() 'db_schema.xml', 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $tableStatements = $this->describeTable->describeShard('default'); $tableSql = $tableStatements['dependent']; $this->assertRegExp('/CONSTRAINT\s`DEPENDENT_PAGE_ID_ON_TEST_TABLE_PAGE_ID`/', $tableSql); @@ -337,10 +347,11 @@ public function testForeignKeyReferenceId() /** * @moduleName Magento_TestSetupDeclarationModule1 * @moduleName Magento_TestSetupDeclarationModule8 + * @throws \Exception */ public function testDisableIndexByExternalModule() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1', 'Magento_TestSetupDeclarationModule8'] ); $this->moduleManager->updateRevision( @@ -367,7 +378,7 @@ public function testDisableIndexByExternalModule() 'module.xml', 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $tableStatements = $this->describeTable->describeShard('default'); $tableSql = $tableStatements['test_table']; $this->assertNotRegExp( @@ -376,4 +387,36 @@ public function testDisableIndexByExternalModule() 'Index is not being disabled by external module' ); } + + /** + * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule9 + * @dataProviderFromFile Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php + * @throws \Exception + */ + public function testInstallationWithDisablingTables() + { + $modules = [ + 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule9', + ]; + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'disabling_tables', + 'db_schema.xml', + 'etc' + ); + } + $this->cliCommand->install($modules); + + $diff = $this->schemaDiff->diff( + $this->schemaConfig->getDeclarationConfig(), + $this->schemaConfig->getDbConfig() + ); + self::assertNull($diff->getAll()); + $shardData = $this->describeTable->describeShard(Sharding::DEFAULT_CONNECTION); + self::assertEquals($this->getData(), $shardData); + } } diff --git a/dev/tests/static/framework/Magento/ruleset.xml b/dev/tests/static/framework/Magento/ruleset.xml index d0f0d38f8fa03..9dde4b7bd7d32 100644 --- a/dev/tests/static/framework/Magento/ruleset.xml +++ b/dev/tests/static/framework/Magento/ruleset.xml @@ -84,5 +84,6 @@ <rule ref="Squiz.Commenting.DocCommentAlignment"/> <rule ref="Squiz.Functions.GlobalFunction"/> + <rule ref="Squiz.Operators.ValidLogicalOperators"/> <rule ref="Squiz.WhiteSpace.LogicalOperatorSpacing"/> </ruleset> diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/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/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 index 1c23f8d8ccf8a..10c0da47cb2d2 100644 --- 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 @@ -63,6 +63,16 @@ 'type' => 'module', 'name' => 'Magento_AsynchronousOperations', 'path' => 'Model/ResourceModel/System/Message/Collection/Synchronized/Plugin.php' + ], + [ + 'type' => 'module', + 'name' => 'Magento_AuthorizenetAcceptjs', + 'path' => 'Gateway/Validator/TransactionHashValidator.php' + ], + [ + 'type' => 'module', + 'name' => 'Magento_Authorizenet', + 'path' => 'Model/Directpost/Response.php' ] ] ], 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 @@ <item> <path>dev/build/publication/sanity/ce.xml</path> </item> + <item> + <path>app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less</path> + <word>rma</word> + </item> </whitelist> </config> 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..3e788c1eba0ee 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,8 @@ 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 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" > - <testsuite name="Magento Unit Tests"> + <testsuite name="Magento_Unit_Tests_App_Code"> <directory suffix="Test.php">../../../app/code/*/*/Test/Unit</directory> + </testsuite> + <testsuite name="Magento_Unit_Tests_Other"> <directory suffix="Test.php">../../../lib/internal/*/*/Test/Unit</directory> <directory suffix="Test.php">../../../lib/internal/*/*/*/Test/Unit</directory> <directory suffix="Test.php">../../../setup/src/*/*/Test/Unit</directory> 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/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/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/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/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 @@ <?php /** - * Name builder - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\Code; +/** + * Builds namespace with classname out of the parts. + * + * @api + */ class NameBuilder { /** @@ -23,7 +26,7 @@ public function buildClassName($parts) $separator = '\\'; $string = join($separator, $parts); $string = str_replace('_', $separator, $string); - $className = str_replace(' ', $separator, ucwords(str_replace($separator, ' ', $string))); + $className = ucwords($string, $separator); return $className; } } diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 86266ec23fe47..90186707177c9 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -2915,6 +2915,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 +2945,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}} <= ?", 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 <core@magentocommerce.com> */ -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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\DB\Test\Unit\DB\Statement; + +use Magento\Framework\DB\Statement\Parameter; +use Magento\Framework\DB\Statement\Pdo\Mysql; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritdoc + */ +class MysqlTest extends TestCase +{ + /** + * @var \Zend_Db_Adapter_Abstract|MockObject + */ + private $adapterMock; + + /** + * @var \PDO|MockObject + */ + private $pdoMock; + + /** + * @var \Zend_Db_Profiler|MockObject + */ + private $zendDbProfilerMock; + + /** + * @var \PDOStatement|MockObject + */ + private $pdoStatementMock; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->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/Data/Collection.php b/lib/internal/Magento/Framework/Data/Collection.php index dbafc9734e091..9c789e81913c4 100644 --- a/lib/internal/Magento/Framework/Data/Collection.php +++ b/lib/internal/Magento/Framework/Data/Collection.php @@ -7,7 +7,6 @@ use Magento\Framework\Data\Collection\EntityFactoryInterface; use Magento\Framework\Option\ArrayInterface; -use Magento\Framework\Exception\InputException; /** * Data collection @@ -235,20 +234,12 @@ protected function _setIsLoaded($flag = true) * Get current collection page * * @param int $displacement - * @throws \Magento\Framework\Exception\InputException * @return int */ public function getCurPage($displacement = 0) { if ($this->_curPage + $displacement < 1) { return 1; - } elseif ($this->_curPage > $this->getLastPageNumber() && $displacement === 0) { - throw new InputException( - __( - 'currentPage value %1 specified is greater than the %2 page(s) available.', - [$this->_curPage, $this->getLastPageNumber()] - ) - ); } elseif ($this->_curPage + $displacement > $this->getLastPageNumber()) { return $this->getLastPageNumber(); } else { diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php index 80c256d8553ef..daadeae2ac0e2 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php @@ -144,19 +144,6 @@ public function testGetCurPage() $this->assertEquals(1, $this->_model->getCurPage()); } - /** - * Test for getCurPage with exception. - * - * @expectedException \Magento\Framework\Exception\StateException - * @return void - */ - public function testGetCurPageWithException() - { - $this->_model->setCurPage(10); - $this->expectException(\Magento\Framework\Exception\InputException::class); - $this->_model->getCurPage(); - } - /** * Test for method possibleFlowWithItem. * 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 @@ +<?php +/** + * Protocol validator + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Encryption; + +use Magento\Framework\Config\ConfigOptionsListConstants; + +/** + * Encryption Key Validator + */ +class KeyValidator +{ + /** + * Validate encryption key + * + * Validate that encryption key is exactly 32 characters long and has + * no trailing spaces, no invisible characters (tabs, new lines, etc.) + * + * @param string $value + * @return bool + */ + public function isValid($value) : bool + { + return strlen($value) === ConfigOptionsListConstants::STORE_KEY_RANDOM_STRING_SIZE + && preg_match('/^\S+$/', $value); + } +} diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/SodiumChachaIetfTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/SodiumChachaIetfTest.php index 8092ce55b42c8..f90cd4eea5a82 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/SodiumChachaIetfTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/SodiumChachaIetfTest.php @@ -11,13 +11,17 @@ */ namespace Magento\Framework\Encryption\Test\Unit\Adapter; -class SodiumChachaIetfTest extends \PHPUnit\Framework\TestCase +use Magento\Framework\Encryption\Adapter\SodiumChachaIetf; +use PHPUnit\Framework\TestCase; + +class SodiumChachaIetfTest extends TestCase { + /** + * @return array + */ public function getCryptData(): array { - $fixturesFilename = __DIR__ . '/../Crypt/_files/_sodium_chachaieft_fixtures.php'; - - $result = include $fixturesFilename; + $result = include __DIR__ . '/../Crypt/_files/_sodium_chachaieft_fixtures.php'; /* Restore encoded string back to binary */ foreach ($result as &$cryptParams) { $cryptParams['encrypted'] = base64_decode($cryptParams['encrypted']); @@ -29,10 +33,15 @@ public function getCryptData(): array /** * @dataProvider getCryptData + * + * @param string $key + * @param string $encrypted + * @param string $decrypted + * @throws \SodiumException */ - public function testEncrypt(string $key, string $encrypted, string $decrypted) + public function testEncrypt(string $key, string $encrypted, string $decrypted): void { - $crypt = new \Magento\Framework\Encryption\Adapter\SodiumChachaIetf($key); + $crypt = new SodiumChachaIetf($key); $result = $crypt->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Encryption\Test\Unit; + +use Magento\Framework\Encryption\KeyValidator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class KeyValidatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var KeyValidator + */ + private $keyValidator; + + protected function setUp() + { + $this->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/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/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('<two>three</two>')); } } 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/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/Lock/Backend/Cache.php b/lib/internal/Magento/Framework/Lock/Backend/Cache.php new file mode 100644 index 0000000000000..61818cbb8c53c --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/Cache.php @@ -0,0 +1,52 @@ +<?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\Cache\FrontendInterface; + +/** + * Implementation of the lock manager on the basis of the caching system. + */ +class Cache implements \Magento\Framework\Lock\LockManagerInterface +{ + /** + * @var FrontendInterface + */ + private $cache; + + /** + * @param FrontendInterface $cache + */ + public function __construct(FrontendInterface $cache) + { + $this->cache = $cache; + } + /** + * @inheritdoc + */ + public function lock(string $name, int $timeout = -1): bool + { + return $this->cache->save('1', $name, [], $timeout); + } + + /** + * @inheritdoc + */ + public function unlock(string $name): bool + { + return $this->cache->remove($name); + } + + /** + * @inheritdoc + */ + public function isLocked(string $name): bool + { + return (bool)$this->cache->test($name); + } +} 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/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/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/Model/ResourceModel/ResourceModelPool.php b/lib/internal/Magento/Framework/Model/ResourceModel/ResourceModelPool.php new file mode 100644 index 0000000000000..f62619f16e1d1 --- /dev/null +++ b/lib/internal/Magento/Framework/Model/ResourceModel/ResourceModelPool.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Model\ResourceModel; + +use Magento\Framework\ObjectManagerInterface; + +/** + * Pool of resource model instances per entity + */ +class ResourceModelPool implements ResourceModelPoolInterface +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + + /** + * @inheritdoc + */ + public function get(string $resourceClassName): AbstractResource + { + return $this->objectManager->get($resourceClassName); + } +} diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/ResourceModelPoolInterface.php b/lib/internal/Magento/Framework/Model/ResourceModel/ResourceModelPoolInterface.php new file mode 100644 index 0000000000000..0274bb6504a0c --- /dev/null +++ b/lib/internal/Magento/Framework/Model/ResourceModel/ResourceModelPoolInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Model\ResourceModel; + +/** + * Pool of resource model instances per entity + * + * @api + */ +interface ResourceModelPoolInterface +{ + /** + * Return instance for given class name from pool. + * + * @param string $resourceClassName + * @return AbstractResource + */ + public function get(string $resourceClassName): AbstractResource; +} 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/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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Reflection\Test\Unit\Fixture\UseClasses; + +class SampleOne +{ + +} diff --git a/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleOne/SampleThree.php b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleOne/SampleThree.php new file mode 100644 index 0000000000000..5384bfa6a6779 --- /dev/null +++ b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleOne/SampleThree.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Reflection\Test\Unit\Fixture\UseClasses\SampleOne; + +class SampleThree +{ + +} diff --git a/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleTwo.php b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleTwo.php new file mode 100644 index 0000000000000..02e2c7da30cd4 --- /dev/null +++ b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleTwo.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Reflection\Test\Unit\Fixture\UseClasses; + +class SampleTwo +{ + +} diff --git a/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleTwo/SampleFour.php b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleTwo/SampleFour.php new file mode 100644 index 0000000000000..160d4c5132203 --- /dev/null +++ b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleTwo/SampleFour.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Reflection\Test\Unit\Fixture\UseClasses\SampleTwo; + +class SampleFour +{ + +} diff --git a/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseSample.php b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseSample.php new file mode 100644 index 0000000000000..88c1b4602065b --- /dev/null +++ b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseSample.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Reflection\Test\Unit\Fixture; + +use Magento\Framework\Reflection\Test\Unit\Fixture\UseClasses\SampleOne; +use Magento\Framework\Reflection\Test\Unit\Fixture\UseClasses\SampleTwo as Sample2; + +class UseSample +{ + // ... +} diff --git a/lib/internal/Magento/Framework/Reflection/Test/Unit/TypeProcessorTest.php b/lib/internal/Magento/Framework/Reflection/Test/Unit/TypeProcessorTest.php index 86a4693d9e5b6..1a8702c0e1c5b 100644 --- a/lib/internal/Magento/Framework/Reflection/Test/Unit/TypeProcessorTest.php +++ b/lib/internal/Magento/Framework/Reflection/Test/Unit/TypeProcessorTest.php @@ -8,9 +8,18 @@ use Magento\Framework\Exception\SerializationException; use Magento\Framework\Reflection\Test\Unit\Fixture\TSample; +use Magento\Framework\Reflection\Test\Unit\Fixture\TSampleInterface; +use Magento\Framework\Reflection\Test\Unit\Fixture\UseClasses\SampleOne; +use Magento\Framework\Reflection\Test\Unit\Fixture\UseClasses\SampleOne\SampleThree; +use Magento\Framework\Reflection\Test\Unit\Fixture\UseClasses\SampleTwo; +use Magento\Framework\Reflection\Test\Unit\Fixture\UseClasses\SampleTwo\SampleFour; +use Magento\Framework\Reflection\Test\Unit\Fixture\UseSample; use Magento\Framework\Reflection\TypeProcessor; use Zend\Code\Reflection\ClassReflection; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class TypeProcessorTest extends \PHPUnit\Framework\TestCase { /** @@ -278,7 +287,7 @@ public function arrayParamTypeDataProvider() { return [ ['method name' => '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..f4d83ece134cf 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php @@ -86,6 +86,7 @@ public function query(RequestInterface $request) $response = [ 'documents' => $documents, 'aggregations' => $aggregations, + 'total' => count($documents) ]; return $this->responseFactory->create($response); } 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..51e7ea9be0c24 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 @@ -26,7 +26,7 @@ class Match implements QueryInterface /** * @var string */ - const SPECIAL_CHARACTERS = '+~/\\<>\'":*$#@()!,.?`=%&^'; + const SPECIAL_CHARACTERS = '-+~/\\<>\'":*$#@()!,.?`=%&^'; const MINIMAL_CHARACTER_LENGTH = 3; 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..a35e1fd8b6151 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,6 +155,7 @@ public function testQuery() 'aggregation2' => [2, 4], ], ], + 'total' => 1 ]; $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) 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 @@ <xs:field xpath="@name" /> </xs:key> </xs:element> - <xs:element type="xs:string" name="from" /> - <xs:element type="xs:string" name="size" /> + <xs:element type="xs:string" name="from" minOccurs="0" /> + <xs:element type="xs:string" name="size" minOccurs="0" /> </xs:sequence> <xs:attribute type="xs:string" name="query" use="required" /> <xs:attribute type="xs:string" name="index" use="required" /> 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 @@ <xs:include schemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/operations.xsd" /> <xs:attributeGroup name="baseConstraint"> - <xs:attributeGroup ref="basicOperations" /> + <xs:attribute name="disabled" type="xs:boolean" /> <xs:attribute name="referenceId" type="referenceIdType" use="required" /> </xs:attributeGroup> </xs:schema> 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 @@ <xs:element name="constraint" /> <xs:element name="index" type="index" /> </xs:choice> + <xs:attributeGroup ref="basicOperations" /> <xs:attribute name="name" type="xs:string" use="required" /> <xs:attribute name="resource" type="resourceType" /> <xs:attribute name="engine" type="engineType" /> 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/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 <column xmlns:xsi="xsi" xsi:type="integer" name="first_column" nullable="1" unsigned="0"/> <column xmlns:xsi="xsi" xsi:type="date" name="second_column" nullable="0"/> - <constraint xmlns:xsi="xsi" xsi:type="foreign" name="some_foreign_constraint" + <constraint xmlns:xsi="xsi" xsi:type="foreign" referenceId="some_foreign_constraint" referenceTable="table" referenceColumn="column" table="first_table" column="first_column"/> - <constraint xmlns:xsi="xsi" xsi:type="primary" name="PRIMARY"> + <constraint xmlns:xsi="xsi" xsi:type="primary" referenceId="PRIMARY"> <column name="second_column"/> </constraint> - <index name="TEST_INDEX" indexType="btree"> + <index referenceId="TEST_INDEX" indexType="btree"> <column name="first_column"/> </index> </table> 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/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/Layout/etc/head.xsd b/lib/internal/Magento/Framework/View/Layout/etc/head.xsd index a913507ae17b3..15762dc2f0ae6 100644 --- a/lib/internal/Magento/Framework/View/Layout/etc/head.xsd +++ b/lib/internal/Magento/Framework/View/Layout/etc/head.xsd @@ -20,6 +20,15 @@ <xs:attribute name="type" type="xs:string"/> <xs:attribute name="order" type="xs:integer"/> <xs:attribute name="src_type" type="xs:string"/> + <xs:attribute name="as"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:enumeration value="font" /> + <xs:enumeration value="script" /> + <xs:enumeration value="style" /> + </xs:restriction> + </xs:simpleType> + </xs:attribute> </xs:complexType> <xs:complexType name="metaType"> diff --git a/lib/internal/Magento/Framework/View/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/Webapi/ServiceInputProcessor.php b/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php index 26102f008c7c3..a9b553f6dd6f9 100644 --- a/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php +++ b/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php @@ -165,17 +165,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 +192,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; + } } } 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/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..4499c314ce6ca 100644 --- a/lib/web/css/source/lib/_resets.less +++ b/lib/web/css/source/lib/_resets.less @@ -4,7 +4,7 @@ // */ // -// Resetes +// Resets // _____________________________________________ // @@ -105,6 +105,13 @@ .lib-css(box-shadow, @focus__box-shadow); } } + + input[type="radio"], + input[type="checkbox"] { + &:focus { + box-shadow: none; + } + } } // 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/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..dfa35473176b9 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -1777,7 +1777,8 @@ valid = true, validateConfig = { errorElement: 'label', - ignore: '.ignore-validate' + ignore: '.ignore-validate', + hideError: false }, form, validator, classes, elementValue; @@ -1815,7 +1816,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; } diff --git a/nginx.conf.sample b/nginx.conf.sample index 90604808f6ec0..ce3891627bc8c 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -99,7 +99,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 +108,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 +117,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"; } 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..765d0a616f77c 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -29136,1463 +29136,6 @@ vars.put("adminImportFilePath", filepath); </stringProp> </hashTree> - <ThroughputController guiclass="ThroughputControllerGui" testclass="ThroughputController" testname="Export Products" enabled="true"> - <intProp name="ThroughputController.style">1</intProp> - <boolProp name="ThroughputController.perThread">false</boolProp> - <intProp name="ThroughputController.maxThroughput">1</intProp> - <stringProp name="ThroughputController.percentThroughput">${exportProductsPercentage}</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx</stringProp></ThroughputController> - <hashTree> - <JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Set Test Label" enabled="true"> - <stringProp name="script"> -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()); -} - </stringProp> - <stringProp name="scriptLanguage">javascript</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/setup_label.jmx</stringProp></JSR223PreProcessor> - <hashTree/> - <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Set Label" enabled="true"> - <stringProp name="BeanShellSampler.query"> - vars.put("testLabel", "Export Products"); - </stringProp> - <boolProp name="BeanShellSampler.resetInterpreter">true</boolProp> - </BeanShellSampler> - <hashTree/> - - <JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Get admin form key PostProcessor" enabled="true"> - <stringProp name="script"> - 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); - } - </stringProp> - <stringProp name="scriptLanguage">javascript</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin/handle_admin_form_key.jmx</stringProp></JSR223PostProcessor> - <hashTree/> - <JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Set admin form key PreProcessor" enabled="true"> - <stringProp name="script"> - 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); - } - } - } - </stringProp> - <stringProp name="scriptLanguage">javascript</stringProp> - </JSR223PreProcessor> - <hashTree/> - - <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true"> - <collectionProp name="CookieManager.cookies"/> - <boolProp name="CookieManager.clearEachIteration">false</boolProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/http_cookie_manager_without_clear_each_iteration.jmx</stringProp></CookieManager> - <hashTree/> - - <GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Admin Login" enabled="true"> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/simple_controller.jmx</stringProp> -</GenericController> - <hashTree> - <CriticalSectionController guiclass="CriticalSectionControllerGui" testclass="CriticalSectionController" testname="Admin Login Lock" enabled="true"> - <stringProp name="CriticalSectionController.lockName">get-admin-email</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/lock_controller.jmx</stringProp></CriticalSectionController> - <hashTree> - - <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Get Admin Email" enabled="true"> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/get_admin_email.jmx</stringProp> - <stringProp name="BeanShellSampler.query"> -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); - </stringProp> - <stringProp name="BeanShellSampler.filename"/> - <stringProp name="BeanShellSampler.parameters"/> - <boolProp name="BeanShellSampler.resetInterpreter">true</boolProp> - </BeanShellSampler> - <hashTree/> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="SetUp - Login" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"/> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_login/admin_login.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert login form shown" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="-1397214398">Welcome</stringProp> - <stringProp name="-515240035"><title>Magento Admin</title></stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">2</intProp> - </ResponseAssertion> - <hashTree/> - <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract form key" enabled="true"> - <stringProp name="RegexExtractor.useHeaders">false</stringProp> - <stringProp name="RegexExtractor.refname">admin_form_key</stringProp> - <stringProp name="RegexExtractor.regex"><input name="form_key" type="hidden" value="([^'"]+)" /></stringProp> - <stringProp name="RegexExtractor.template">$1$</stringProp> - <stringProp name="RegexExtractor.default"/> - <stringProp name="RegexExtractor.match_number">1</stringProp> - </RegexExtractor> - <hashTree/> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert form_key extracted" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="2845929">^.+$</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">1</intProp> - <stringProp name="Assertion.scope">variable</stringProp> - <stringProp name="Scope.variable">admin_form_key</stringProp> - </ResponseAssertion> - <hashTree/> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="SetUp - Login Submit Form" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"> - <elementProp name="dummy" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">dummy</stringProp> - </elementProp> - <elementProp name="form_key" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_form_key}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">form_key</stringProp> - </elementProp> - <elementProp name="login[password]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_password}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">login[password]</stringProp> - </elementProp> - <elementProp name="login[username]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_user}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">login[username]</stringProp> - </elementProp> - </collectionProp> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/dashboard/</stringProp> - <stringProp name="HTTPSampler.method">POST</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <stringProp name="HTTPSampler.implementation">Java</stringProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_login/admin_login_submit_form.jmx</stringProp> - </HTTPSamplerProxy> - <hashTree> - <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract form key" enabled="true"> - <stringProp name="RegexExtractor.useHeaders">false</stringProp> - <stringProp name="RegexExtractor.refname">admin_form_key</stringProp> - <stringProp name="RegexExtractor.regex"><input name="form_key" type="hidden" value="([^'"]+)" /></stringProp> - <stringProp name="RegexExtractor.template">$1$</stringProp> - <stringProp name="RegexExtractor.default"/> - <stringProp name="RegexExtractor.match_number">1</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_login/admin_retrieve_form_key.jmx</stringProp></RegexExtractor> - <hashTree/> - </hashTree> - </hashTree> - - <GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Simple Controller" enabled="true"> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/simple_controller.jmx</stringProp> -</GenericController> - <hashTree> - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Export Page" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"/> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/export/</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/common/export.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert success" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="1723813687">Export Settings</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">2</intProp> - </ResponseAssertion> - <hashTree/> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Export Products" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"> - <elementProp name="form_key" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">form_key</stringProp> - <stringProp name="Argument.value">${admin_form_key}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="attribute_code" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">attribute_code</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[allow_message][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[allow_message][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[allow_open_amount]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[allow_open_amount]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[category_ids]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[category_ids]</stringProp> - <stringProp name="Argument.value">24,25,26,27,28,29,30</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[configurable_variations]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[configurable_variations]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[cost][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[cost][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[country_of_manufacture]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[country_of_manufacture]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[created_at]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[created_at]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[custom_design]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[custom_design]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[custom_design_from][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[custom_design_from][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[custom_design_to][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[custom_design_to][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[custom_layout_update]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[custom_layout_update]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[description]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[description]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[email_template]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[email_template]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[gallery]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[gallery]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[gift_message_available]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[gift_message_available]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[gift_wrapping_available]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[gift_wrapping_available]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[gift_wrapping_price][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[gift_wrapping_price][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[group_price][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[group_price][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[has_options]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[has_options]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[image]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[image]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[image_label]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[image_label]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[is_redeemable][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[is_redeemable][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[is_returnable]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[is_returnable]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[lifetime][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[lifetime][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[links_exist][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[links_exist][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[links_purchased_separately][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[links_purchased_separately][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[links_title]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[links_title]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[media_gallery]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[media_gallery]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[meta_description]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[meta_description]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[meta_keyword]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[meta_keyword]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[meta_title]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[meta_title]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[minimal_price][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[minimal_price][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[msrp][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[msrp][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[msrp_display_actual_price_type]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[msrp_display_actual_price_type]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[name]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[name]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[news_from_date][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[news_from_date][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[news_to_date][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[news_to_date][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[old_id][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[old_id][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[open_amount_max][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[open_amount_max][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[open_amount_min][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[open_amount_min][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[options_container]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[options_container]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[page_layout]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[page_layout]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[price][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[price][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[price_type][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[price_type][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[price_view]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[price_view]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[quantity_and_stock_status]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[quantity_and_stock_status]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[related_tgtr_position_behavior][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[related_tgtr_position_behavior][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[related_tgtr_position_limit][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[related_tgtr_position_limit][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[required_options]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[required_options]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[samples_title]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[samples_title]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[shipment_type][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[shipment_type][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[short_description]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[short_description]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[sku]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[sku]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[sku_type][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[sku_type][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[small_image]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[small_image]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[small_image_label]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[small_image_label]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[special_from_date][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[special_from_date][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[special_price][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[special_price][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[special_to_date][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[special_to_date][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[status]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[status]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[tax_class_id]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[tax_class_id]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[thumbnail]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[thumbnail]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[thumbnail_label]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[thumbnail_label]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[tier_price][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[tier_price][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[updated_at]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[updated_at]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[upsell_tgtr_position_behavior][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[upsell_tgtr_position_behavior][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[upsell_tgtr_position_limit][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[upsell_tgtr_position_limit][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[url_key]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[url_key]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[url_path]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[url_path]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[use_config_allow_message][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[use_config_allow_message][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[use_config_email_template][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[use_config_email_template][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[use_config_is_redeemable][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[use_config_is_redeemable][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[use_config_lifetime][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[use_config_lifetime][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[visibility]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[visibility]</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[weight][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[weight][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="export_filter[weight_type][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">export_filter[weight_type][]</stringProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - <elementProp name="frontend_label" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.name">frontend_label</stringProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - </elementProp> - </collectionProp> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/export/export/entity/catalog_product/file_format/csv</stringProp> - <stringProp name="HTTPSampler.method">POST</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/export_products/export_products.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="-261088822">Simple Product 1</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">16</intProp> - </ResponseAssertion> - <hashTree/> - </hashTree> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"/> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/auth/logout/</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/setup/admin_logout.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - - <BeanShellPostProcessor guiclass="TestBeanGUI" testclass="BeanShellPostProcessor" testname="Return Admin to Pool" enabled="true"> - <boolProp name="resetInterpreter">false</boolProp> - <stringProp name="parameters"/> - <stringProp name="filename"/> - <stringProp name="script"> - adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - if (adminUsersDistribution == 1) { - adminUserList = props.get("adminUserList"); - adminUserList.add(vars.get("admin_user")); - } - </stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/common/return_admin_email_to_pool.jmx</stringProp></BeanShellPostProcessor> - <hashTree/> - </hashTree> - </hashTree> - - - <ThroughputController guiclass="ThroughputControllerGui" testclass="ThroughputController" testname="Export Customers" enabled="true"> - <intProp name="ThroughputController.style">1</intProp> - <boolProp name="ThroughputController.perThread">false</boolProp> - <intProp name="ThroughputController.maxThroughput">1</intProp> - <stringProp name="ThroughputController.percentThroughput">${exportCustomersPercentage}</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx</stringProp></ThroughputController> - <hashTree> - <JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Set Test Label" enabled="true"> - <stringProp name="script"> -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()); -} - </stringProp> - <stringProp name="scriptLanguage">javascript</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/setup_label.jmx</stringProp></JSR223PreProcessor> - <hashTree/> - <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Set Label" enabled="true"> - <stringProp name="BeanShellSampler.query"> - vars.put("testLabel", "Export Customers"); - </stringProp> - <boolProp name="BeanShellSampler.resetInterpreter">true</boolProp> - </BeanShellSampler> - <hashTree/> - - <JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Get admin form key PostProcessor" enabled="true"> - <stringProp name="script"> - 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); - } - </stringProp> - <stringProp name="scriptLanguage">javascript</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin/handle_admin_form_key.jmx</stringProp></JSR223PostProcessor> - <hashTree/> - <JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Set admin form key PreProcessor" enabled="true"> - <stringProp name="script"> - 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); - } - } - } - </stringProp> - <stringProp name="scriptLanguage">javascript</stringProp> - </JSR223PreProcessor> - <hashTree/> - - <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true"> - <collectionProp name="CookieManager.cookies"/> - <boolProp name="CookieManager.clearEachIteration">false</boolProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/http_cookie_manager_without_clear_each_iteration.jmx</stringProp></CookieManager> - <hashTree/> - - <GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Admin Login" enabled="true"> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/simple_controller.jmx</stringProp> -</GenericController> - <hashTree> - <CriticalSectionController guiclass="CriticalSectionControllerGui" testclass="CriticalSectionController" testname="Admin Login Lock" enabled="true"> - <stringProp name="CriticalSectionController.lockName">get-admin-email</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/lock_controller.jmx</stringProp></CriticalSectionController> - <hashTree> - - <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Get Admin Email" enabled="true"> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/get_admin_email.jmx</stringProp> - <stringProp name="BeanShellSampler.query"> -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); - </stringProp> - <stringProp name="BeanShellSampler.filename"/> - <stringProp name="BeanShellSampler.parameters"/> - <boolProp name="BeanShellSampler.resetInterpreter">true</boolProp> - </BeanShellSampler> - <hashTree/> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="SetUp - Login" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"/> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_login/admin_login.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert login form shown" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="-1397214398">Welcome</stringProp> - <stringProp name="-515240035"><title>Magento Admin</title></stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">2</intProp> - </ResponseAssertion> - <hashTree/> - <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract form key" enabled="true"> - <stringProp name="RegexExtractor.useHeaders">false</stringProp> - <stringProp name="RegexExtractor.refname">admin_form_key</stringProp> - <stringProp name="RegexExtractor.regex"><input name="form_key" type="hidden" value="([^'"]+)" /></stringProp> - <stringProp name="RegexExtractor.template">$1$</stringProp> - <stringProp name="RegexExtractor.default"/> - <stringProp name="RegexExtractor.match_number">1</stringProp> - </RegexExtractor> - <hashTree/> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert form_key extracted" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="2845929">^.+$</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">1</intProp> - <stringProp name="Assertion.scope">variable</stringProp> - <stringProp name="Scope.variable">admin_form_key</stringProp> - </ResponseAssertion> - <hashTree/> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="SetUp - Login Submit Form" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"> - <elementProp name="dummy" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">dummy</stringProp> - </elementProp> - <elementProp name="form_key" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_form_key}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">form_key</stringProp> - </elementProp> - <elementProp name="login[password]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_password}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">login[password]</stringProp> - </elementProp> - <elementProp name="login[username]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_user}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">login[username]</stringProp> - </elementProp> - </collectionProp> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/dashboard/</stringProp> - <stringProp name="HTTPSampler.method">POST</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <stringProp name="HTTPSampler.implementation">Java</stringProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_login/admin_login_submit_form.jmx</stringProp> - </HTTPSamplerProxy> - <hashTree> - <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract form key" enabled="true"> - <stringProp name="RegexExtractor.useHeaders">false</stringProp> - <stringProp name="RegexExtractor.refname">admin_form_key</stringProp> - <stringProp name="RegexExtractor.regex"><input name="form_key" type="hidden" value="([^'"]+)" /></stringProp> - <stringProp name="RegexExtractor.template">$1$</stringProp> - <stringProp name="RegexExtractor.default"/> - <stringProp name="RegexExtractor.match_number">1</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_login/admin_retrieve_form_key.jmx</stringProp></RegexExtractor> - <hashTree/> - </hashTree> - </hashTree> - - <GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Simple Controller" enabled="true"> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/simple_controller.jmx</stringProp> -</GenericController> - <hashTree> - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Export Page" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"/> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/export/</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/common/export.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert success" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="1723813687">Export Settings</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">2</intProp> - </ResponseAssertion> - <hashTree/> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Export Customers" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"> - <elementProp name="form_key" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_form_key}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">form_key</stringProp> - <stringProp name="Argument.desc">false</stringProp> - </elementProp> - <elementProp name="attribute_code" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">attribute_code</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[confirmation]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[confirmation]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[created_at]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[created_at]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[created_in]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[created_in]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[default_billing][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[default_billing][]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[default_shipping][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[default_shipping][]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[disable_auto_group_change]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[disable_auto_group_change]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[dob][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[dob][]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[email]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[email]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[firstname]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[firstname]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[gender]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[gender]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[group_id]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[group_id]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[lastname]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[lastname]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[middlename]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[middlename]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[password_hash]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[password_hash]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[prefix]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[prefix]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[reward_update_notification][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[reward_update_notification][]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[reward_warning_notification][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[reward_warning_notification][]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[rp_token]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[rp_token]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[rp_token_created_at][]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">,</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[rp_token_created_at][]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[store_id]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[store_id]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[suffix]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[suffix]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[taxvat]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[taxvat]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="export_filter[website_id]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">export_filter[website_id]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="frontend_label" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">frontend_label</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - </collectionProp> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/export/export/entity/customer/file_format/csv</stringProp> - <stringProp name="HTTPSampler.method">POST</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/export_customers/export_customers.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="-2040454917">user_1@example.com</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">16</intProp> - </ResponseAssertion> - <hashTree/> - </hashTree> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"/> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/auth/logout/</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/setup/admin_logout.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - - <BeanShellPostProcessor guiclass="TestBeanGUI" testclass="BeanShellPostProcessor" testname="Return Admin to Pool" enabled="true"> - <boolProp name="resetInterpreter">false</boolProp> - <stringProp name="parameters"/> - <stringProp name="filename"/> - <stringProp name="script"> - adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - if (adminUsersDistribution == 1) { - adminUserList = props.get("adminUserList"); - adminUserList.add(vars.get("admin_user")); - } - </stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/common/return_admin_email_to_pool.jmx</stringProp></BeanShellPostProcessor> - <hashTree/> - </hashTree> - </hashTree> - - <ThroughputController guiclass="ThroughputControllerGui" testclass="ThroughputController" testname="API" enabled="true"> <intProp name="ThroughputController.style">1</intProp> <boolProp name="ThroughputController.perThread">false</boolProp> 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/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/Model/ConfigOptionsList.php b/setup/src/Magento/Setup/Model/ConfigOptionsList.php index fa79139e73313..afe1a5d9e2591 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,6 +39,11 @@ class ConfigOptionsList implements ConfigOptionsListInterface */ private $configOptionsCollection = []; + /** + * @var KeyValidator + */ + private $encryptionKeyValidator; + /** * @var array */ @@ -52,18 +58,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 +173,7 @@ public function getOptions() } /** - * {@inheritdoc} + * @inheritdoc */ public function createConfig(array $data, DeploymentConfig $deploymentConfig) { @@ -184,7 +197,7 @@ public function createConfig(array $data, DeploymentConfig $deploymentConfig) } /** - * {@inheritdoc} + * @inheritdoc */ public function validate(array $options, DeploymentConfig $deploymentConfig) { @@ -276,8 +289,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/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/Model/ConfigOptionsListTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php index f342a11493498..d7f680309c9ef 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php @@ -144,7 +144,7 @@ public function testValidateEmptyEncryptionKey() ConfigOptionsListConstants::INPUT_KEY_ENCRYPTION_KEY => '' ]; $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) ); } 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="<?= $hints['encrypt_key'] ?>" tooltip-trigger="focus" tooltip-append-to-body="true" - ng-minlength="4" + ng-minlength="32" + ng-maxlength="32" + ng-pattern="/^\S+$/" required > <div class="error-container"> <span ng-show="webconfig.key.$error.required"> You must enter an encryption key. </span> - <span ng-show="webconfig.key.$error.minlength"> - Your encryption key must be longer and stronger. + <span ng-show="webconfig.key.$error.minlength + || webconfig.key.$error.maxlength + || webconfig.key.$error.pattern" + > + Encryption key must be 32 character string without any white space. </span> </div> </div> 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;