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 @@ - + When you enable this option your site may slow down. Magento\Config\Model\Config\Source\Yesno - + 1 - + Magento\Config\Model\Config\Source\Yesno When you enable this option your site may slow down. 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 @@ + + + +
+ +
+
\ 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 @@ + + + + + + + + <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/AuthorizenetAcceptjs/etc/config.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml index 24291187c0584..7324421d3c14b 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml @@ -7,6 +7,13 @@ --> <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> diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/payment.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/payment.xml index b8292839c3bd1..b9f8d40b03006 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/etc/payment.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/payment.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/payment.xsd"> <methods> <method name="authorizenet_acceptjs"> - <allow_multiple_address>1</allow_multiple_address> + <allow_multiple_address>0</allow_multiple_address> </method> </methods> </payment> 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/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/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/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/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/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/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/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/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/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 e3318db505489..cab8e013d9ba1 100644 --- a/app/code/Magento/Catalog/Model/CategoryList.php +++ b/app/code/Magento/Catalog/Model/CategoryList.php @@ -76,11 +76,10 @@ public function getList(SearchCriteriaInterface $searchCriteria) $this->extensionAttributesJoinProcessor->process($collection); $this->collectionProcessor->process($searchCriteria, $collection); - $collection->load(); $items = []; - foreach ($collection->getItems() as $category) { - $items[] = $this->categoryRepository->get($category->getId()); + foreach ($collection->getAllIds() as $id) { + $items[] = $this->categoryRepository->get($id); } /** @var CategorySearchResultsInterface $searchResult */ 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/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 9cf1431317944..3d7f863b7c0d3 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php @@ -99,7 +99,7 @@ protected function _isApplicableAttribute($object, $attribute) */ 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(); @@ -491,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/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/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 1eb76e66677c9..3afdc41888c79 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -241,6 +241,25 @@ <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> @@ -342,6 +361,32 @@ </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"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml index 0082b376bc4a6..46329dde278bc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml @@ -148,4 +148,66 @@ <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 a3b4203b7a69e..2914ecc470220 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml @@ -45,6 +45,16 @@ <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> @@ -54,4 +64,31 @@ <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 f0367fb72c6a2..c9d70319c2877 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml @@ -272,4 +272,20 @@ <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/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/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml index a11c3fd0d7afa..27167d03d528e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -104,4 +104,10 @@ <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 bf0762b4b0319..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,6 +157,69 @@ <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> @@ -159,4 +264,46 @@ <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/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index d136661e917cb..0b3a31194ea36 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -96,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> @@ -110,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> @@ -708,6 +745,129 @@ <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> @@ -729,4 +889,21 @@ <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 cb8bb47f3cc93..e5070340421a9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml @@ -42,4 +42,18 @@ <data key="price_1">24.00</data> <data key="qty_1">15</data> </entity> -</entities> + <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/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/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml new file mode 100644 index 0000000000000..e23a503266e33 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminNewWidgetPage" url="admin/widget_instance/new/" area="admin" module="Magento_Widget"> + <section name="AdminNewWidgetSelectProductPopupSection"/> + </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/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/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 ee6af87b8e2c5..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,10 +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/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 d3aaeefdc6bb2..5efd04eacb719 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml @@ -10,12 +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/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/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 a1bb27bb45a29..bc7c472df6eac 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml @@ -19,9 +19,14 @@ <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/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index 3f67e4b087cc4..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,31 @@ <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']" 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')]" 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]']"/> @@ -47,17 +55,21 @@ <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"/> @@ -194,4 +206,4 @@ <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> +</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 43345c69e6c04..939974248aabf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml @@ -28,6 +28,7 @@ <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"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml index be046955a66a5..ef596bed186e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml @@ -9,8 +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']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml index de7ee35a40f8f..1cd64544d9636 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml @@ -26,11 +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 f35eb63ee0e0a..51b5a0242d976 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml @@ -32,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/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 4114b199715cb..8504683648bce 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -13,7 +13,7 @@ <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"/> @@ -74,6 +74,11 @@ <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"/> 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/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/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/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/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/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/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/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/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/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/CategoryListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php index f78c0ad924954..b8b76524099f4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php @@ -93,7 +93,7 @@ public function testGetList() $collection = $this->getMockBuilder(Collection::class)->disableOriginalConstructor()->getMock(); $collection->expects($this->once())->method('getSize')->willReturn($totalCount); - $collection->expects($this->once())->method('getItems')->willReturn([$categoryFirst, $categorySecond]); + $collection->expects($this->once())->method('getAllIds')->willReturn([$categoryIdFirst, $categoryIdSecond]); $this->collectionProcessorMock->expects($this->once()) ->method('process') 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/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/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/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/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/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/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/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 908cf66e3f64d..404c31296e4dd 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -2546,7 +2546,7 @@ public function validateRow(array $rowData, $rowNum) $rowNum, $rowData[self::COL_NAME], $message, - ProcessingError::ERROR_LEVEL_NOT_CRITICAL + $errorLevel ) ->getErrorAggregator() ->addRowToSkip($rowNum); 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/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 98c0f91668f7a..e3f61af771f8c 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -3,18 +3,33 @@ * 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 @@ -60,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 * @@ -89,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( @@ -118,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, @@ -149,7 +225,11 @@ public function __construct( $groupManagement, $connection, $productLimitationFactory, - $metadataPool + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $resourceModelPool ); } @@ -168,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) { @@ -180,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. * 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/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 cacc761dbee36..44b47faf3d4b8 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php @@ -13,6 +13,8 @@ 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 @@ -39,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); } /** @@ -73,10 +84,17 @@ public function execute(\Magento\Framework\Event\Observer $observer) ); if (!empty($this->productUrlRewriteGenerator->generate($product))) { - $this->urlPersist->deleteByData([ - UrlRewrite::ENTITY_ID => $product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - ]); + 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/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/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/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/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/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/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/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/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/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/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/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/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/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/Page/CmsPageEditPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml index 885310d9399ae..73db6b61343b1 100644 --- a/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml +++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml @@ -7,7 +7,7 @@ --> <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"> + <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"/> 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/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/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/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml index 1a7b641070ad8..4e9319351a130 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml @@ -33,7 +33,7 @@ <argument name="userInput" type="string"/> </arguments> <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" visible="false" stepKey="openTaxCalcSettingsSection"/> - <scrollTo selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="goToCheckbox"/> + <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"/> @@ -49,11 +49,11 @@ <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" visible="false" stepKey="openTaxCalcSettingsSection"/> - <scrollTo selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="goToCheckbox"/> + <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> \ No newline at end of file +</actionGroups> 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/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/Model/Product/Type/Configurable/Price.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php index bee334596e990..f2bf3116af9e4 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php @@ -7,14 +7,15 @@ */ namespace Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; + +/** + * Class Price for configurable product + */ class Price extends \Magento\Catalog\Model\Product\Type\Price { /** - * Get product final price - * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float + * @inheritdoc */ public function getFinalPrice($qty, $product) { @@ -22,7 +23,10 @@ public function getFinalPrice($qty, $product) return $product->getCalculatedFinalPrice(); } if ($product->getCustomOption('simple_product') && $product->getCustomOption('simple_product')->getProduct()) { - $finalPrice = parent::getFinalPrice($qty, $product->getCustomOption('simple_product')->getProduct()); + /** @var Product $simpleProduct */ + $simpleProduct = $product->getCustomOption('simple_product')->getProduct(); + $simpleProduct->setCustomerGroupId($product->getCustomerGroupId()); + $finalPrice = parent::getFinalPrice($qty, $simpleProduct); } else { $priceInfo = $product->getPriceInfo(); $finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue(); @@ -35,7 +39,7 @@ public function getFinalPrice($qty, $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPrice($product) { @@ -48,6 +52,7 @@ public function getPrice($product) } } } + return 0; } } diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php new file mode 100644 index 0000000000000..8bdde2aeb0cff --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; + +/** + * Plugin for CommonTaxCollector to apply Tax Class ID from child item for configurable product + */ +class CommonTaxCollector +{ + /** + * Apply Tax Class ID from child item for configurable product + * + * @param \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject + * @param QuoteDetailsItemInterface $result + * @param QuoteDetailsItemInterfaceFactory $itemDataObjectFactory + * @param AbstractItem $item + * @return QuoteDetailsItemInterface + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterMapItem( + \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject, + QuoteDetailsItemInterface $result, + QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, + AbstractItem $item + ) : QuoteDetailsItemInterface { + if ($item->getProduct()->getTypeId() === Configurable::TYPE_CODE && $item->getHasChildren()) { + $childItem = $item->getChildren()[0]; + $result->getTaxClassKey()->setValue($childItem->getProduct()->getTaxClassId()); + } + + return $result; + } +} diff --git a/app/code/Magento/ConfigurableProduct/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/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/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/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/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/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index df95632c4b606..d4780c5c0867a 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -49,7 +49,7 @@ type AddConfigurableProductsToCartOutput { } input ConfigurableProductCartItemInput { - data: CartItemDetailsInput! + data: CartItemInput! variant_sku: String! customizable_options:[CustomizableOptionInput!] } 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/Customer/Block/Adminhtml/Edit/Renderer/Region.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php index 9a025211c9b0a..0aeed1562c51e 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php @@ -48,7 +48,7 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele $regionId = $element->getForm()->getElement('region_id')->getValue(); - $html = '<div class="field field-state required admin__field _required">'; + $html = '<div class="field field-state admin__field">'; $element->setClass('input-text admin__control-text'); $element->setRequired(true); $html .= $element->getLabelHtml() . '<div class="control admin__field-control">'; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php index 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 1bc6bb1da3680..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 @@ -77,7 +77,8 @@ protected function _construct() { parent::_construct(); $this->setId('customer_view_cart_grid'); - $this->setDefaultSort('added_at', 'desc'); + $this->setDefaultSort('added_at'); + $this->setDefaultDir('desc'); $this->setSortable(false); $this->setPagerVisibility(false); $this->setFilterVisibility(false); 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/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/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/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/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 0e31f0e0c7782..bed0c71ae7fad 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml @@ -10,6 +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']//strong[contains(text(), '{{var}}')]" 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/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/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/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index 4a45c4ad48d19..c31742519e581 100644 --- a/app/code/Magento/Customer/etc/frontend/di.xml +++ b/app/code/Magento/Customer/etc/frontend/di.xml @@ -57,7 +57,7 @@ <type name="Magento\Checkout\Block\Cart\Sidebar"> <plugin name="customer_cart" type="Magento\Customer\Model\Cart\ConfigPlugin" /> </type> - <type name="Magento\Framework\Session\SessionManager"> + <type name="Magento\Framework\Session\SessionManagerInterface"> <plugin name="session_checker" type="Magento\Customer\CustomerData\Plugin\SessionChecker" /> </type> <type name="Magento\Authorization\Model\CompositeUserContext"> @@ -77,4 +77,4 @@ </argument> </arguments> </type> -</config> +</config> \ No newline at end of file diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml index fd5ecbfa7f277..0c5af453f2373 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml @@ -6,6 +6,9 @@ */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <head> + <title>Create New Customer Account + diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml index d49dae6dee58f..3518df736c4ac 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml @@ -6,6 +6,9 @@ */ --> + + Customer Login + diff --git a/app/code/Magento/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 @@ +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 @@ + + + + + + + Magento\Customer\Api\Data\CustomerInterface::EMAIL + + + + \ No newline at end of file diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index 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/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 42716d73373a2..1ad8b79ad12f3 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -1964,9 +1964,7 @@ 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; } /** diff --git a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php index ac458024fb65c..c3d82ef34a448 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Dhl\Test\Unit\Model; use Magento\Dhl\Model\Carrier; @@ -34,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) @@ -81,11 +81,6 @@ class CarrierTest extends \PHPUnit\Framework\TestCase */ private $xmlValidator; - /** - * @var Request|MockObject - */ - private $request; - /** * @var LoggerInterface|MockObject */ @@ -108,25 +103,6 @@ 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); $this->error = $this->getMockBuilder(Error::class) @@ -194,7 +170,7 @@ 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', @@ -214,6 +190,11 @@ public function scopeConfigGetValue($path) return isset($pathMap[$path]) ? $pathMap[$path] : null; } + /** + * Prepare shipping label content test + * + * @throws \ReflectionException + */ public function testPrepareShippingLabelContent() { $xml = simplexml_load_file( @@ -225,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 @@ -235,6 +218,8 @@ public function testPrepareShippingLabelContentException(\SimpleXMLElement $xml) } /** + * Prepare shipping label content exception data provider + * * @return array */ public function prepareShippingLabelContentExceptionDataProvider() @@ -254,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) { @@ -283,9 +271,9 @@ public function testCollectRates() ->willReturn($responseXml); $this->coreDateMock->method('date') - ->willReturnCallback(function () { - return date(\DATE_RFC3339); - }); + ->willReturnCallback(function () { + return date(\DATE_RFC3339); + }); $request = $this->objectManager->getObject(RateRequest::class, $requestData); @@ -338,13 +326,15 @@ public function testCollectRatesErrorMessage() /** * Test request to shipment sends valid xml values. * + * @dataProvider requestToShipmentDataProvider * @param string $origCountryId * @param string $expectedRegionCode - * @dataProvider requestToShipmentDataProvider + * @param string $destCountryId + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \ReflectionException */ - public function testRequestToShipment(string $origCountryId, string $expectedRegionCode) + public function testRequestToShipment(string $origCountryId, string $expectedRegionCode, string $destCountryId) { - $expectedRequestXml = file_get_contents(__DIR__ . '/_files/shipment_request.xml'); $scopeConfigValueMap = [ ['carriers/dhl/account', 'store', null, '1234567890'], ['carriers/dhl/gateway_url', 'store', null, 'https://xmlpi-ea.dhl.com/XMLShippingServlet'], @@ -361,6 +351,54 @@ public function testRequestToShipment(string $origCountryId, string $expectedReg $this->httpResponse->method('getBody') ->willReturn(utf8_encode(file_get_contents(__DIR__ . '/_files/response_shipping_label.xml'))); + $request = $this->getRequest($origCountryId, $destCountryId); + + $this->logger->method('debug') + ->with($this->stringContains('********')); + + $result = $this->model->requestToShipment($request); + + $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() + ); + } + + /** + * Prepare and retrieve request object + * + * @param string $origCountryId + * @param string $destCountryId + * @return Request|MockObject + */ + private function getRequest(string $origCountryId, string $destCountryId) + { + $order = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + $order->method('getSubtotal') + ->willReturn('10.00'); + + $shipment = $this->getMockBuilder(Order\Shipment::class) + ->disableOriginalConstructor() + ->getMock(); + $shipment->method('getOrder') + ->willReturn($order); + $packages = [ 'package' => [ 'params' => [ @@ -381,62 +419,77 @@ public function testRequestToShipment(string $origCountryId, string $expectedReg ], ]; - $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($origCountryId); - $this->request->method('setPackages') - ->willReturnSelf(); - $this->request->method('setPackageWeight') - ->willReturnSelf(); - $this->request->method('setPackageValue') - ->willReturnSelf(); - $this->request->method('setValueWithDiscount') - ->willReturnSelf(); - $this->request->method('setPackageCustomsValue') - ->willReturnSelf(); - $this->request->method('setFreeMethodWeight') - ->willReturnSelf(); - $this->request->method('getPackageWeight') - ->willReturn('0.454000000001'); - $this->request->method('getFreeMethodWeight') - ->willReturn('0.454000000001'); - $this->request->method('getOrderShipment') - ->willReturn($shipment); + foreach ($methods as $method => $return) { + $return ? $request->method($method)->willReturn($return) : $request->method($method)->willReturnSelf(); + } - $this->logger->method('debug') - ->with($this->stringContains('********')); + return $request; + } - $result = $this->model->requestToShipment($this->request); + /** + * Prepare and retrieve expected request xml element + * + * @param string $origCountryId + * @param string $destCountryId + * @return Element + */ + private function getExpectedRequestXml(string $origCountryId, string $destCountryId, string $regionCode) + { + $requestXmlPath = $origCountryId == $destCountryId + ? '/_files/domestic_shipment_request.xml' + : '/_files/shipment_request.xml'; - $reflectionClass = new \ReflectionObject($this->httpClient); - $rawPostData = $reflectionClass->getProperty('raw_post_data'); - $rawPostData->setAccessible(true); + $expectedRequestElement = new Element(file_get_contents(__DIR__ . $requestXmlPath)); - $this->assertNotNull($result); - $requestXml = $rawPostData->getValue($this->httpClient); - $requestElement = new Element($requestXml); - $this->assertEquals($expectedRegionCode, $requestElement->RegionCode->__toString()); - $requestElement->RegionCode = 'Checked'; - $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'; - $expectedRequestElement = new Element($expectedRequestXml); - $this->assertXmlStringEqualsXmlString($expectedRequestElement->asXML(), $requestElement->asXML()); + $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]; } /** @@ -448,17 +501,21 @@ public function requestToShipmentDataProvider() { return [ [ - 'GB', 'EU' + 'GB', 'EU', 'US' ], [ - 'SG', 'AP' + 'SG', 'AP', 'US' + ], + [ + 'DE', 'EU', 'DE' ] ]; } /** - * @dataProvider dhlProductsDataProvider + * Get DHL products test * + * @dataProvider dhlProductsDataProvider * @param string $docType * @param array $products */ @@ -468,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 +596,8 @@ public function testBuildMessageReference($servicePrefix) } /** + * Build message reference data provider + * * @return array */ public function buildMessageReferenceDataProvider() @@ -581,6 +642,8 @@ public function testBuildSoftwareName($productName) } /** + * Data provider for testBuildSoftwareName + * * @return array */ public function buildSoftwareNameDataProvider() @@ -610,6 +673,8 @@ public function testBuildSoftwareVersion($productVersion) } /** + * Data provider for testBuildSoftwareVersion + * * @return array */ public function buildSoftwareVersionProvider() @@ -695,6 +760,8 @@ private function getRateMethodFactory(): MockObject } /** + * Get config reader + * * @return MockObject */ private function getConfigReader(): MockObject @@ -709,6 +776,8 @@ private function getConfigReader(): MockObject } /** + * Get read factory + * * @return MockObject */ private function getReadFactory(): MockObject @@ -727,6 +796,8 @@ private function getReadFactory(): MockObject } /** + * Get store manager + * * @return MockObject */ private function getStoreManager(): MockObject @@ -748,6 +819,8 @@ private function getStoreManager(): MockObject } /** + * Get carrier helper + * * @return CarrierHelper */ private function getCarrierHelper(): CarrierHelper @@ -766,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/domestic_shipment_request.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml new file mode 100644 index 0000000000000..b71c2fa4a7dde --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml @@ -0,0 +1,88 @@ + + + + + + currentTime + MAGE_SHIP_28TO32_Char_CHECKED + some ID + some password + + + CHECKED + N + N + EN + Y + + 1234567890 + S + 1234567890 + S + 1234567890 + + + + + + + + + + + + + + + 1 + + + shipment reference + St + + + 1 + + + 1 + CP + 0.454 + 3 + 3 + 3 + item_name + + + 0.454 + K + + + currentTime + DHL Parcel + DD + C + CP + USD + + + 1234567890 + + 1234567890 + + + + + + + + + + + PDF + \ No newline at end of file diff --git a/app/code/Magento/Dhl/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 @@
- + + Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic) Magento\Dhl\Model\Source\Contenttype @@ -81,18 +82,12 @@ - + Magento\Dhl\Model\Source\Method\Doc - - D - - + Magento\Dhl\Model\Source\Method\Nondoc - - N - diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php index ffbcce11cb4f6..5339b0c9eb5bd 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php @@ -216,7 +216,7 @@ protected function _getRatesByCode($code, $toCurrencies = null) $connection = $this->getConnection(); $bind = [':currency_from' => $code]; $select = $connection->select()->from( - $this->getTable('directory_currency_rate'), + $this->_currencyRateTable, ['currency_to', 'rate'] )->where( 'currency_from = :currency_from' diff --git a/app/code/Magento/Downloadable/Helper/Download.php b/app/code/Magento/Downloadable/Helper/Download.php index 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 @@ + + + + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/AdminProductDropdownOrderSection.xml new file mode 100644 index 0000000000000..39b4e303d5165 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Section/AdminProductDropdownOrderSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml new file mode 100755 index 0000000000000..55740af4d834f --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml @@ -0,0 +1,31 @@ + + + + + + + + + + <description value="After selecting a downloadable product when adding Admin should be switch to simple implicitly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10929"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="downloadable"/> + </actionGroup> + <!-- Fill form for Virtual Product Type --> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/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/Eav/Model/Attribute/Data/File.php b/app/code/Magento/Eav/Model/Attribute/Data/File.php index f14e01accef07..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'])) { + 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/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/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/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/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 9e2659a757924..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,12 +134,13 @@ 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) { $groupPriceKey = $this->fieldNameResolver->getFieldName( $priceAttribute, - array_merge($context, ['customerGroupId' => $group->getId()]) + ['customerGroupId' => $group->getId(), 'websiteId' => $context['websiteId']] ); $allAttributes[$groupPriceKey] = [ 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_FLOAT), diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php index 5abe4972e34d0..535ab62dd5991 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php @@ -17,13 +17,19 @@ class Converter implements ConverterInterface */ private const ES_NO_INDEX = 'no'; + /** + * Text flags for Elasticsearch no analyze index value + */ + private const ES_NO_ANALYZE = 'not_analyzed'; + /** * Mapping between internal data types and elastic service. * * @var array */ private $mapping = [ - 'no_index' => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_INDEX_VALUE => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_ANALYZE_VALUE => self::ES_NO_ANALYZE, ]; /** diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php index 02c4d29558dad..5ecfd62430032 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php @@ -17,6 +17,7 @@ interface ConverterInterface */ public const INTERNAL_NO_INDEX_VALUE = 'no_index'; public const INTERNAL_INDEX_VALUE = 'index'; + public const INTERNAL_NO_ANALYZE_VALUE = 'no_analyze'; /** * Get service field index type. diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php index 3208ca7fc6171..5f319daeb3b39 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php @@ -51,22 +51,22 @@ public function __construct( */ public function getFieldName(AttributeAdapter $attribute, $context = []): ?string { - $fieldType = $this->fieldTypeResolver->getFieldType($attribute); $attributeCode = $attribute->getAttributeCode(); - $frontendInput = $attribute->getFrontendInput(); if (empty($context['type'])) { - $fieldName = $attributeCode; - } elseif ($context['type'] === FieldMapperInterface::TYPE_FILTER) { + return $attributeCode; + } + + $fieldType = $this->fieldTypeResolver->getFieldType($attribute); + $frontendInput = $attribute->getFrontendInput(); + $fieldName = $attributeCode; + if ($context['type'] === FieldMapperInterface::TYPE_FILTER) { if ($this->isStringServiceFieldType($fieldType)) { - return $this->getFieldName( - $attribute, - array_merge($context, ['type' => FieldMapperInterface::TYPE_QUERY]) - ); + return $this->getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode); } $fieldName = $attributeCode; } elseif ($context['type'] === FieldMapperInterface::TYPE_QUERY) { $fieldName = $this->getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode); - } else { + } elseif ($context['type'] == FieldMapperInterface::TYPE_SORT && $attribute->isSortable()) { $fieldName = 'sort_' . $attributeCode; } @@ -115,10 +115,11 @@ private function getQueryTypeFieldName($frontendInput, $fieldType, $attributeCod private function getRefinedFieldName($frontendInput, $fieldType, $attributeCode) { $stringTypeKey = $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING); + $keywordTypeKey = $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD); switch ($frontendInput) { case 'select': case 'multiselect': - return in_array($fieldType, [$stringTypeKey, 'integer'], true) + return in_array($fieldType, [$stringTypeKey, $keywordTypeKey, 'integer'], true) ? $attributeCode . '_value' : $attributeCode; case 'boolean': diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php index 4fa16db98639e..652fc853adbcc 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php @@ -24,7 +24,9 @@ class SpecialAttribute implements ResolverInterface */ public function getFieldName(AttributeAdapter $attribute, $context = []): ?string { - if (in_array($attribute->getAttributeCode(), ['id', 'sku', 'store_id', 'visibility'], true)) { + if (in_array($attribute->getAttributeCode(), ['id', 'sku', 'store_id', 'visibility'], true) + && empty($context['type']) + ) { return $attribute->getAttributeCode(); } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php index b5c78cbc28f45..6876b23bbb156 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php @@ -7,6 +7,7 @@ namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider; +use Magento\Framework\App\ObjectManager; use Magento\Eav\Model\Config; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; @@ -19,6 +20,7 @@ as FieldTypeResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ResolverInterface as FieldIndexResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; /** * Provide static fields for mapping of product. @@ -55,6 +57,11 @@ class StaticField implements FieldProviderInterface */ private $fieldIndexResolver; + /** + * @var FieldName\ResolverInterface + */ + private $fieldNameResolver; + /** * @param Config $eavConfig * @param FieldTypeConverterInterface $fieldTypeConverter @@ -62,6 +69,7 @@ class StaticField implements FieldProviderInterface * @param FieldTypeResolver $fieldTypeResolver * @param FieldIndexResolver $fieldIndexResolver * @param AttributeProvider $attributeAdapterProvider + * @param FieldName\ResolverInterface|null $fieldNameResolver */ public function __construct( Config $eavConfig, @@ -69,7 +77,8 @@ public function __construct( IndexTypeConverterInterface $indexTypeConverter, FieldTypeResolver $fieldTypeResolver, FieldIndexResolver $fieldIndexResolver, - AttributeProvider $attributeAdapterProvider + AttributeProvider $attributeAdapterProvider, + FieldName\ResolverInterface $fieldNameResolver = null ) { $this->eavConfig = $eavConfig; $this->fieldTypeConverter = $fieldTypeConverter; @@ -77,6 +86,8 @@ public function __construct( $this->fieldTypeResolver = $fieldTypeResolver; $this->fieldIndexResolver = $fieldIndexResolver; $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->fieldNameResolver = $fieldNameResolver ?: ObjectManager::getInstance() + ->get(FieldName\ResolverInterface::class); } /** @@ -84,6 +95,7 @@ public function __construct( * * @param array $context * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getFields(array $context = []): array { @@ -92,19 +104,38 @@ public function getFields(array $context = []): array foreach ($attributes as $attribute) { $attributeAdapter = $this->attributeAdapterProvider->getByAttributeCode($attribute->getAttributeCode()); - $code = $attributeAdapter->getAttributeCode(); + $fieldName = $this->fieldNameResolver->getFieldName($attributeAdapter); - $allAttributes[$code] = [ + $allAttributes[$fieldName] = [ 'type' => $this->fieldTypeResolver->getFieldType($attributeAdapter), ]; $index = $this->fieldIndexResolver->getFieldIndex($attributeAdapter); if (null !== $index) { - $allAttributes[$code]['index'] = $index; + $allAttributes[$fieldName]['index'] = $index; + } + + if ($attributeAdapter->isSortable()) { + $sortFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $allAttributes[$fieldName]['fields'][$sortFieldName] = [ + 'type' => $this->fieldTypeConverter->convert( + FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD + ), + 'index' => $this->indexTypeConverter->convert( + IndexTypeConverterInterface::INTERNAL_NO_ANALYZE_VALUE + ) + ]; } if ($attributeAdapter->isComplexType()) { - $allAttributes[$code . '_value'] = [ + $childFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_QUERY] + ); + $allAttributes[$childFieldName] = [ 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING) ]; } diff --git a/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php new file mode 100644 index 0000000000000..b3f8a56110f8d --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch\Model\Advanced; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\Config; +use Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyInterface; + +/** + * Strategy interface for preparing product collection. + */ +class ProductCollectionPrepareStrategy implements ProductCollectionPrepareStrategyInterface +{ + /** + * @var Config + */ + private $catalogConfig; + + /** + * @param Config $catalogConfig + */ + public function __construct( + Config $catalogConfig + ) { + $this->catalogConfig = $catalogConfig; + } + + /** + * @inheritdoc + */ + public function prepare(Collection $collection) + { + $collection + ->addAttributeToSelect($this->catalogConfig->getProductAttributes()) + ->addMinimalPrice() + ->addTaxPercents(); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/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 eeb48f805bccf..0c03a9df18dc8 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php @@ -22,15 +22,13 @@ public function build( array $queryResult, DataProviderInterface $dataProvider ) { - $buckets = $queryResult['aggregations'][$bucket->getName()]['buckets'] ?? []; $values = []; - foreach ($buckets as $resultBucket) { + foreach ($queryResult['aggregations'][$bucket->getName()]['buckets'] as $resultBucket) { $values[$resultBucket['key']] = [ 'value' => $resultBucket['key'], 'count' => $resultBucket['doc_count'], ]; } - return $values; } } 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 e83c49941bcc2..aaa9d8a88382f 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php @@ -5,8 +5,6 @@ */ namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; -use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\Request\Query\BoolExpression; use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; @@ -28,31 +26,20 @@ class Match implements QueryInterface private $fieldMapper; /** - * @deprecated - * @see \Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer * @var PreprocessorInterface[] */ protected $preprocessorContainer; - /** - * @var ValueTransformerPool - */ - private $valueTransformerPool; - /** * @param FieldMapperInterface $fieldMapper * @param PreprocessorInterface[] $preprocessorContainer - * @param ValueTransformerPool|null $valueTransformerPool */ public function __construct( FieldMapperInterface $fieldMapper, - array $preprocessorContainer, - ValueTransformerPool $valueTransformerPool = null + array $preprocessorContainer ) { $this->fieldMapper = $fieldMapper; $this->preprocessorContainer = $preprocessorContainer; - $this->valueTransformerPool = $valueTransformerPool ?? ObjectManager::getInstance() - ->get(ValueTransformerPool::class); } /** @@ -85,6 +72,10 @@ public function build(array $selectQuery, RequestQueryInterface $requestQuery, $ */ protected function prepareQuery($queryValue, $conditionType) { + $queryValue = $this->escape($queryValue); + foreach ($this->preprocessorContainer as $preprocessor) { + $queryValue = $preprocessor->process($queryValue); + } $condition = $conditionType === BoolExpression::QUERY_CONDITION_NOT ? self::QUERY_CONDITION_MUST_NOT : $conditionType; return [ @@ -113,34 +104,21 @@ protected function buildQueries(array $matches, array $queryValue) // Checking for quoted phrase \"phrase test\", trim escaped surrounding quotes if found $count = 0; - $value = preg_replace('#^"(.*)"$#m', '$1', $queryValue['value'], -1, $count); + $value = preg_replace('#^\\\\"(.*)\\\\"$#m', '$1', $queryValue['value'], -1, $count); $condition = ($count) ? 'match_phrase' : 'match'; - $attributesTypes = $this->fieldMapper->getAllAttributesTypes(); - $transformedTypes = []; foreach ($matches as $match) { $resolvedField = $this->fieldMapper->getFieldName( $match['field'], ['type' => FieldMapperInterface::TYPE_QUERY] ); - $valueTransformer = $this->valueTransformerPool->get($attributesTypes[$resolvedField]['type'] ?? 'text'); - $valueTransformerHash = \spl_object_hash($valueTransformer); - if (!isset($transformedTypes[$valueTransformerHash])) { - $transformedTypes[$valueTransformerHash] = $valueTransformer->transform($value); - } - $transformedValue = $transformedTypes[$valueTransformerHash]; - if (null === $transformedValue) { - //Value is incompatible with this field type. - continue; - } - $conditions[] = [ 'condition' => $queryValue['condition'], 'body' => [ $condition => [ $resolvedField => [ - 'query' => $transformedValue, - 'boost' => $match['boost'] ?? 1, + 'query' => $value, + 'boost' => isset($match['boost']) ? $match['boost'] : 1, ], ], ], @@ -153,13 +131,16 @@ protected function buildQueries(array $matches, array $queryValue) /** * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. * - * @deprecated - * @see \Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer + * 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 */ protected function escape($value) { + $value = preg_replace('/@+|[@+-]+$/', '', $value); + $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; $replace = '\\\$1'; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php new file mode 100644 index 0000000000000..5ccf202e3812b --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface + as FieldNameResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Framework\Search\RequestInterface; + +/** + * Sort builder. + */ +class Sort +{ + /** + * List of fields that need to skipp by default. + */ + private const DEFAULT_SKIPPED_FIELDS = [ + 'entity_id', + ]; + + /** + * Default mapping for special fields. + */ + private const DEFAULT_MAP = [ + 'relevance' => '_score', + ]; + + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + + /** + * @var array + */ + private $skippedFields; + + /** + * @var array + */ + private $map; + + /** + * @param AttributeProvider $attributeAdapterProvider + * @param FieldNameResolver $fieldNameResolver + * @param array $skippedFields + * @param array $map + */ + public function __construct( + AttributeProvider $attributeAdapterProvider, + FieldNameResolver $fieldNameResolver, + array $skippedFields = [], + array $map = [] + ) { + $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->fieldNameResolver = $fieldNameResolver; + $this->skippedFields = array_merge(self::DEFAULT_SKIPPED_FIELDS, $skippedFields); + $this->map = array_merge(self::DEFAULT_MAP, $map); + } + + /** + * Prepare sort. + * + * @param RequestInterface $request + * @return array + */ + public function getSort(RequestInterface $request) + { + $sorts = []; + foreach ($request->getSort() as $item) { + if (in_array($item['field'], $this->skippedFields)) { + continue; + } + $attribute = $this->attributeAdapterProvider->getByAttributeCode($item['field']); + $fieldName = $this->fieldNameResolver->getFieldName($attribute); + if (isset($this->map[$fieldName])) { + $fieldName = $this->map[$fieldName]; + } + if ($attribute->isSortable() && !($attribute->isFloatType() || $attribute->isIntegerType())) { + $suffix = $this->fieldNameResolver->getFieldName( + $attribute, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $fieldName .= '.' . $suffix; + } + $sorts[] = [ + $fieldName => [ + 'order' => strtolower($item['direction']) + ] + ]; + } + + return $sorts; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php deleted file mode 100644 index 49eca6e9d82a6..0000000000000 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; - -use Magento\Elasticsearch\Model\Adapter\FieldType\Date; -use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; - -/** - * Value transformer for date type fields. - */ -class DateTransformer implements ValueTransformerInterface -{ - /** - * @var Date - */ - private $dateFieldType; - - /** - * @param Date $dateFieldType - */ - public function __construct(Date $dateFieldType) - { - $this->dateFieldType = $dateFieldType; - } - - /** - * @inheritdoc - */ - public function transform(string $value): ?string - { - try { - $formattedDate = $this->dateFieldType->formatDate(null, $value); - } catch (\Exception $e) { - $formattedDate = null; - } - - return $formattedDate; - } -} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php deleted file mode 100644 index 5e330076d3df7..0000000000000 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; - -use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; - -/** - * Value transformer for float type fields. - */ -class FloatTransformer implements ValueTransformerInterface -{ - /** - * @inheritdoc - */ - public function transform(string $value): ?float - { - return \is_numeric($value) ? (float) $value : null; - } -} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php deleted file mode 100644 index 0846ff3a9bd86..0000000000000 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; - -use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; - -/** - * Value transformer for integer type fields. - */ -class IntegerTransformer implements ValueTransformerInterface -{ - /** - * @inheritdoc - */ - public function transform(string $value): ?int - { - return \is_numeric($value) ? (int) $value : null; - } -} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php deleted file mode 100644 index 68bec2580f621..0000000000000 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; - -use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; -use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; - -/** - * Value transformer for fields with text types. - */ -class TextTransformer implements ValueTransformerInterface -{ - /** - * @var PreprocessorInterface[] - */ - private $preprocessors; - - /** - * @param PreprocessorInterface[] $preprocessors - */ - public function __construct(array $preprocessors = []) - { - foreach ($preprocessors as $preprocessor) { - if (!$preprocessor instanceof PreprocessorInterface) { - throw new \InvalidArgumentException( - \sprintf('"%s" is not a instance of ValueTransformerInterface.', get_class($preprocessor)) - ); - } - } - - $this->preprocessors = $preprocessors; - } - - /** - * @inheritdoc - */ - public function transform(string $value): string - { - $value = $this->escape($value); - foreach ($this->preprocessors as $preprocessor) { - $value = $preprocessor->process($value); - } - - return $value; - } - - /** - * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. - * - * @param string $value - * @return string - */ - private function escape(string $value): string - { - $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; - $replace = '\\\$1'; - - return preg_replace($pattern, $replace, $value); - } -} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php deleted file mode 100644 index c84ddc69cc7a8..0000000000000 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\SearchAdapter\Query; - -/** - * Value transformer of search term for matching with ES field types. - */ -interface ValueTransformerInterface -{ - /** - * Transform value according to field type. - * - * @param string $value - * @return mixed - */ - public function transform(string $value); -} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php deleted file mode 100644 index 11a35d79ce1fd..0000000000000 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\SearchAdapter\Query; - -/** - * Pool of value transformers. - */ -class ValueTransformerPool -{ - /** - * @var ValueTransformerInterface[] - */ - private $transformers; - - /** - * @param ValueTransformerInterface[] $valueTransformers - */ - public function __construct(array $valueTransformers = []) - { - foreach ($valueTransformers as $valueTransformer) { - if (!$valueTransformer instanceof ValueTransformerInterface) { - throw new \InvalidArgumentException( - \sprintf('"%s" is not a instance of ValueTransformerInterface.', get_class($valueTransformer)) - ); - } - } - - $this->transformers = $valueTransformers; - } - - /** - * Get value transformer related to field type. - * - * @param string $fieldType - * @return ValueTransformerInterface - */ - public function get(string $fieldType): ValueTransformerInterface - { - return $this->transformers[$fieldType] ?? $this->transformers['default']; - } -} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php b/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php index 33fda48f4af57..0813975ac9a4b 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php @@ -76,6 +76,7 @@ public function create($response) [ 'documents' => $documents, 'aggregations' => $aggregations, + 'total' => $response['total'] ] ); } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php index ba5e97aa14b54..7c2a33c05aa08 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php @@ -24,6 +24,7 @@ use Magento\Customer\Api\Data\GroupInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface as FieldNameResolver; +use Magento\Catalog\Model\ResourceModel\Category\Collection; /** * @SuppressWarnings(PHPMD) @@ -65,6 +66,11 @@ class DynamicFieldTest extends \PHPUnit\Framework\TestCase */ private $categoryList; + /** + * @var Collection + */ + private $categoryCollection; + /** * @var FieldNameResolver */ @@ -100,6 +106,10 @@ protected function setUp() $this->categoryList = $this->getMockBuilder(CategoryListInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->categoryCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods(['getAllIds']) + ->getMock(); $objectManager = new ObjectManagerHelper($this); @@ -113,6 +123,7 @@ protected function setUp() 'attributeAdapterProvider' => $this->attributeAdapterProvider, 'categoryList' => $this->categoryList, 'fieldNameResolver' => $this->fieldNameResolver, + 'categoryCollection' => $this->categoryCollection, ] ); } @@ -124,7 +135,6 @@ protected function setUp() * @param $groupId * @param array $expected * @return void - * @throws \Magento\Framework\Exception\LocalizedException */ public function testGetAllAttributesTypes( $complexType, @@ -138,10 +148,6 @@ public function testGetAllAttributesTypes( $this->searchCriteriaBuilder->expects($this->any()) ->method('create') ->willReturn($searchCriteria); - $categorySearchResults = $this->getMockBuilder(CategorySearchResultsInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getItems']) - ->getMockForAbstractClass(); $groupSearchResults = $this->getMockBuilder(GroupSearchResultsInterface::class) ->disableOriginalConstructor() ->setMethods(['getItems']) @@ -156,19 +162,10 @@ public function testGetAllAttributesTypes( $groupSearchResults->expects($this->any()) ->method('getItems') ->willReturn([$group]); - $category = $this->getMockBuilder(CategoryInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getId']) - ->getMockForAbstractClass(); - $category->expects($this->any()) - ->method('getId') - ->willReturn($categoryId); - $categorySearchResults->expects($this->any()) - ->method('getItems') - ->willReturn([$category]); - $this->categoryList->expects($this->any()) - ->method('getList') - ->willReturn($categorySearchResults); + + $this->categoryCollection->expects($this->any()) + ->method('getAllIds') + ->willReturn([$categoryId]); $categoryAttributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php index 4fa99f3bf834d..fd5c87bc9518b 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php @@ -66,6 +66,7 @@ protected function setUp() * @param $fieldType * @param $attributeCode * @param $frontendInput + * @param $isSortable * @param $context * @param $expected * @return void @@ -74,6 +75,7 @@ public function testGetFieldName( $fieldType, $attributeCode, $frontendInput, + $isSortable, $context, $expected ) { @@ -82,7 +84,7 @@ public function testGetFieldName( ->willReturn('string'); $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['getAttributeCode', 'getFrontendInput']) + ->setMethods(['getAttributeCode', 'getFrontendInput', 'isSortable']) ->getMock(); $attributeMock->expects($this->any()) ->method('getAttributeCode') @@ -90,6 +92,9 @@ public function testGetFieldName( $attributeMock->expects($this->any()) ->method('getFrontendInput') ->willReturn($frontendInput); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); $this->fieldTypeResolver->expects($this->any()) ->method('getFieldType') ->willReturn($fieldType); @@ -106,13 +111,13 @@ public function testGetFieldName( public function getFieldNameProvider() { return [ - ['', 'code', '', [], 'code'], - ['', 'code', '', ['type' => 'default'], 'code'], - ['string', '*', '', ['type' => 'default'], '_all'], - ['', 'code', '', ['type' => 'default'], 'code'], - ['', 'code', 'select', ['type' => 'default'], 'code'], - ['', 'code', 'boolean', ['type' => 'default'], 'code'], - ['', 'code', '', ['type' => 'type'], 'sort_code'], + ['', 'code', '', false, [], 'code'], + ['', 'code', '', false, ['type' => 'default'], 'code'], + ['string', '*', '', false, ['type' => 'default'], '_all'], + ['', 'code', '', false, ['type' => 'default'], 'code'], + ['', 'code', 'select', false, ['type' => 'default'], 'code'], + ['', 'code', 'boolean', false, ['type' => 'default'], 'code'], + ['', 'code', '', true, ['type' => 'sort'], 'sort_code'], ]; } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php index bf8b601ed43ab..de85b8b6602b8 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php @@ -20,6 +20,8 @@ as FieldTypeResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ResolverInterface as FieldIndexResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface + as FieldNameResolver; /** * @SuppressWarnings(PHPMD) @@ -61,6 +63,11 @@ class StaticFieldTest extends \PHPUnit\Framework\TestCase */ private $fieldTypeResolver; + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + /** * Set up test environment * @@ -90,6 +97,10 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getFieldIndex']) ->getMock(); + $this->fieldNameResolver = $this->getMockBuilder(FieldNameResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldName']) + ->getMock(); $objectManager = new ObjectManagerHelper($this); @@ -102,6 +113,7 @@ protected function setUp() 'attributeAdapterProvider' => $this->attributeAdapterProvider, 'fieldIndexResolver' => $this->fieldIndexResolver, 'fieldTypeResolver' => $this->fieldTypeResolver, + 'fieldNameResolver' => $this->fieldNameResolver, ] ); } @@ -113,6 +125,10 @@ protected function setUp() * @param $indexType * @param $isComplexType * @param $complexType + * @param $isSortable + * @param $fieldName + * @param $compositeFieldName + * @param $sortFieldName * @param array $expected * @return void */ @@ -122,6 +138,10 @@ public function testGetAllAttributesTypes( $indexType, $isComplexType, $complexType, + $isSortable, + $fieldName, + $compositeFieldName, + $sortFieldName, $expected ) { $this->fieldTypeResolver->expects($this->any()) @@ -132,7 +152,30 @@ public function testGetAllAttributesTypes( ->willReturn($indexType); $this->indexTypeConverter->expects($this->any()) ->method('convert') - ->willReturn('no'); + ->with($this->anything()) + ->will($this->returnCallback( + function ($type) { + if ($type === 'no_index') { + return 'no'; + } elseif ($type === 'no_analyze') { + return 'not_analyzed'; + } + } + )); + $this->fieldNameResolver->expects($this->any()) + ->method('getFieldName') + ->with($this->anything()) + ->will($this->returnCallback( + function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) { + if (empty($context)) { + return $fieldName; + } elseif ($context['type'] === 'sort') { + return $sortFieldName; + } elseif ($context['type'] === 'text') { + return $compositeFieldName; + } + } + )); $productAttributeMock = $this->getMockBuilder(AbstractAttribute::class) ->setMethods(['getAttributeCode']) @@ -146,11 +189,14 @@ public function testGetAllAttributesTypes( $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['isComplexType', 'getAttributeCode']) + ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable']) ->getMock(); $attributeMock->expects($this->any()) ->method('isComplexType') ->willReturn($isComplexType); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); $attributeMock->expects($this->any()) ->method('getAttributeCode') ->willReturn($attributeCode); @@ -166,13 +212,12 @@ function ($type) use ($complexType) { static $callCount = []; $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; - if ($type === 'string') { - return 'string'; - } if ($type === 'string') { return 'string'; } elseif ($type === 'float') { return 'float'; + } elseif ($type === 'keyword') { + return 'string'; } else { return $complexType; } @@ -197,6 +242,10 @@ public function attributeProvider() true, true, 'text', + false, + 'category_ids', + 'category_ids_value', + '', [ 'category_ids' => [ 'type' => 'select', @@ -217,6 +266,10 @@ public function attributeProvider() 'no', false, null, + false, + 'attr_code', + '', + '', [ 'attr_code' => [ 'type' => 'text', @@ -234,6 +287,10 @@ public function attributeProvider() null, false, null, + false, + 'attr_code', + '', + '', [ 'attr_code' => [ 'type' => 'text' @@ -243,6 +300,32 @@ public function attributeProvider() 'index' => 'no' ] ] + ], + [ + 'attr_code', + 'text', + null, + false, + null, + true, + 'attr_code', + '', + 'sort_attr_code', + [ + 'attr_code' => [ + 'type' => 'text', + 'fields' => [ + 'sort_attr_code' => [ + 'type' => 'string', + 'index' => 'not_analyzed' + ] + ] + ], + 'store_id' => [ + 'type' => 'string', + 'index' => 'no' + ] + ] ] ]; } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php index c8aa3db39bd5e..8114feb09d35d 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php @@ -7,8 +7,6 @@ use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Elasticsearch\SearchAdapter\Query\Builder\Match as MatchQueryBuilder; -use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; -use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool; use Magento\Framework\Search\Request\Query\Match as MatchRequestQuery; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit_Framework_MockObject_MockObject as MockObject; @@ -25,48 +23,46 @@ class MatchTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $valueTransformerPoolMock = $this->createMock(ValueTransformerPool::class); - $valueTransformerMock = $this->createMock(ValueTransformerInterface::class); - $valueTransformerPoolMock->method('get') - ->willReturn($valueTransformerMock); - $valueTransformerMock->method('transform') - ->willReturnArgument(0); - $this->matchQueryBuilder = (new ObjectManager($this))->getObject( MatchQueryBuilder::class, [ 'fieldMapper' => $this->getFieldMapper(), 'preprocessorContainer' => [], - 'valueTransformerPool' => $valueTransformerPoolMock, ] ); } /** * Tests that method constructs a correct select query. - * * @see MatchQueryBuilder::build + * + * @dataProvider queryValuesInvariantsProvider + * + * @param string $rawQueryValue + * @param string $errorMessage */ - public function testBuild() + public function testBuild($rawQueryValue, $errorMessage) { - $rawQueryValue = 'query_value'; - $selectQuery = $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'not'); + $this->assertSelectQuery( + $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'not'), + $errorMessage + ); + } - $expectedSelectQuery = [ - 'bool' => [ - 'must_not' => [ - [ - 'match' => [ - 'some_field' => [ - 'query' => $rawQueryValue, - 'boost' => 43, - ], - ], - ], - ], - ], + /** + * @link https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs. + * + * @return array + */ + public function queryValuesInvariantsProvider() + { + return [ + ['query_value', 'Select query field must match simple raw query value.'], + ['query_value+', 'Specifying a trailing plus sign causes InnoDB to report a syntax error.'], + ['query_value-', 'Specifying a trailing minus sign causes InnoDB to report a syntax error.'], + ['query_@value', 'The @ symbol is reserved for use by the @distance proximity search operator.'], + ['query_value+@', 'The @ symbol is reserved for use by the @distance proximity search operator.'], ]; - $this->assertEquals($expectedSelectQuery, $selectQuery); } /** @@ -115,6 +111,30 @@ public function matchProvider() ]; } + /** + * @param array $selectQuery + * @param string $errorMessage + */ + private function assertSelectQuery($selectQuery, $errorMessage) + { + $expectedSelectQuery = [ + 'bool' => [ + 'must_not' => [ + [ + 'match' => [ + 'some_field' => [ + 'query' => 'query_value', + 'boost' => 43, + ], + ], + ], + ], + ], + ]; + + $this->assertEquals($expectedSelectQuery, $selectQuery, $errorMessage); + } + /** * Gets fieldMapper mock object. * diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php new file mode 100644 index 0000000000000..efd9073694129 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Test\Unit\SearchAdapter\Query\Builder; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface + as FieldNameResolver; +use Magento\Framework\Search\RequestInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Elasticsearch\SearchAdapter\Query\Builder\Sort; + +/** + * Class SortTest + */ +class SortTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + + /** + * @var Sort + */ + private $sortBuilder; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->attributeAdapterProvider = $this->getMockBuilder(AttributeProvider::class) + ->disableOriginalConstructor() + ->setMethods(['getByAttributeCode']) + ->getMock(); + $this->fieldNameResolver = $this->getMockBuilder(FieldNameResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldName']) + ->getMock(); + + $this->sortBuilder = (new ObjectManager($this))->getObject( + Sort::class, + [ + 'attributeAdapterProvider' => $this->attributeAdapterProvider, + 'fieldNameResolver' => $this->fieldNameResolver, + ] + ); + } + + /** + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @dataProvider getSortProvider + * @param array $sortItems + * @param $isSortable + * @param $isFloatType + * @param $isIntegerType + * @param $fieldName + * @param array $expected + */ + public function testGetSort( + array $sortItems, + $isSortable, + $isFloatType, + $isIntegerType, + $fieldName, + array $expected + ) { + /** @var MockObject|RequestInterface $request */ + $request = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getSort']) + ->getMockForAbstractClass(); + $request->expects($this->any()) + ->method('getSort') + ->willReturn($sortItems); + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['isSortable', 'isFloatType', 'isIntegerType']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); + $attributeMock->expects($this->any()) + ->method('isFloatType') + ->willReturn($isFloatType); + $attributeMock->expects($this->any()) + ->method('isIntegerType') + ->willReturn($isIntegerType); + $this->attributeAdapterProvider->expects($this->any()) + ->method('getByAttributeCode') + ->with($this->anything()) + ->willReturn($attributeMock); + $this->fieldNameResolver->expects($this->any()) + ->method('getFieldName') + ->with($this->anything()) + ->will($this->returnCallback( + function ($attribute, $context) use ($fieldName) { + if (empty($context)) { + return $fieldName; + } elseif ($context['type'] === 'sort') { + return 'sort_' . $fieldName; + } + } + )); + + $this->assertEquals( + $expected, + $this->sortBuilder->getSort($request) + ); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function getSortProvider() + { + return [ + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ] + ], + null, + null, + null, + null, + [] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'price', + 'direction' => 'DESC' + ], + ], + false, + false, + false, + 'price', + [ + [ + 'price' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'price', + 'direction' => 'DESC' + ], + ], + true, + true, + true, + 'price', + [ + [ + 'price' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'name', + 'direction' => 'DESC' + ], + ], + true, + false, + false, + 'name', + [ + [ + 'name.sort_name' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'not_eav_attribute', + 'direction' => 'DESC' + ], + ], + false, + false, + false, + 'not_eav_attribute', + [ + [ + 'not_eav_attribute' => [ + 'order' => 'desc' + ] + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php index 9ea241b2fbf5c..d89e420457206 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php @@ -79,7 +79,7 @@ public function testCreate() 'itemTwo' => 45, ] ]; - $rawResponse = ['documents' => $documents, 'aggregations' => $aggregations]; + $rawResponse = ['documents' => $documents, 'aggregations' => $aggregations, 'total' => 2]; $exceptedResponse = [ 'documents' => [ @@ -102,6 +102,7 @@ public function testCreate() 'itemTwo' => 45 ], ], + 'total' => 2, ]; $this->documentFactory->expects($this->at(0))->method('create') @@ -118,7 +119,11 @@ public function testCreate() $this->objectManager->expects($this->once())->method('create') ->with( $this->equalTo(\Magento\Framework\Search\Response\QueryResponse::class), - $this->equalTo(['documents' => ['document1', 'document2'], 'aggregations' => 'aggregationsData']) + $this->equalTo([ + 'documents' => ['document1', 'document2'], + 'aggregations' => 'aggregationsData', + 'total' => 2 + ]) ) ->will($this->returnValue('QueryResponseObject')); diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json index a821506f5ef6e..c6ac38c1e4005 100644 --- a/app/code/Magento/Elasticsearch/composer.json +++ b/app/code/Magento/Elasticsearch/composer.json @@ -12,7 +12,7 @@ "magento/module-store": "*", "magento/module-catalog-inventory": "*", "magento/framework": "*", - "elasticsearch/elasticsearch": "~2.0|~5.1" + "elasticsearch/elasticsearch": "~2.0|~5.1|~6.1" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 05a67605ba0e6..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,25 +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> - </argument> - </arguments> - </type> - <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool"> - <arguments> - <argument name="valueTransformers" xsi:type="array"> - <item name="default" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer</item> - <item name="date" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\DateTransformer</item> - <item name="float" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\FloatTransformer</item> - <item name="integer" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\IntegerTransformer</item> - </argument> - </arguments> - </type> - <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer"> - <arguments> - <argument name="preprocessors" xsi:type="array"> - <item name="stopwordsPreprocessor" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\Preprocessor\Stopwords</item> - <item name="synonymsPreprocessor" xsi:type="object">Magento\Search\Adapter\Query\Preprocessor\Synonyms</item> + <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/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 cb268b51f08f9..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> @@ -29,7 +29,7 @@ <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> <entity name="ApiGroupedProduct2" 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> 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/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/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/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/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 393383bb2e772..2f6dd2da9bbc4 100644 --- a/app/code/Magento/Msrp/Helper/Data.php +++ b/app/code/Magento/Msrp/Helper/Data.php @@ -7,14 +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; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; /** * Msrp data helper + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Data extends AbstractHelper { @@ -43,6 +46,11 @@ class Data extends AbstractHelper */ protected $productRepository; + /** + * @var MsrpPriceCalculatorInterface + */ + private $msrpPriceCalculator; + /** * @param Context $context * @param StoreManagerInterface $storeManager @@ -51,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, @@ -59,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; @@ -68,6 +78,8 @@ public function __construct( $this->config = $config; $this->priceCurrency = $priceCurrency; $this->productRepository = $productRepository; + $this->msrpPriceCalculator = $msrpPriceCalculator + ?: ObjectManager::getInstance()->get(MsrpPriceCalculatorInterface::class); } /** @@ -78,6 +90,7 @@ public function __construct( * @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) { @@ -139,6 +154,7 @@ public function isShowPriceOnGesture($product) * * @param int|Product $product * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function isShowBeforeOrderConfirm($product) { @@ -149,31 +165,16 @@ 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) { - $msrp = $product->getTypeInstance()->getChildrenMsrp($product); - } elseif ($product->getTypeId() === Configurable::TYPE_CODE) { - $prices = []; - foreach ($product->getTypeInstance()->getUsedProducts($product) as $item) { - if ($item->getMsrp() !== null) { - $prices[] = $item->getMsrp(); - } - } - - $msrp = $prices ? max($prices) : 0; - } else { - return false; - } - } 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/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 e3099aa2f14d6..a2e6da6de5387 100644 --- a/app/code/Magento/Msrp/composer.json +++ b/app/code/Magento/Msrp/composer.json @@ -10,8 +10,6 @@ "magento/module-catalog": "*", "magento/module-downloadable": "*", "magento/module-eav": "*", - "magento/module-grouped-product": "*", - "magento/module-configurable-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 a951c14cf4c70..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 @@ -21,19 +21,7 @@ $priceType = $block->getPrice(); $product = $block->getSaleableItem(); $productId = $product->getId(); -$amount = 0; -if ($product->getMsrp()) { - $amount = $product->getMsrp(); -} elseif ($product->getTypeId() === \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE) { - $amount = $product->getTypeInstance()->getChildrenMsrp($product); -} elseif ($product->getTypeId() === \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE) { - foreach ($product->getTypeInstance()->getUsedProducts($product) as $item) { - if ($item->getMsrp() !== null) { - $prices[] = $item->getMsrp(); - } - } - $amount = $prices ? max($prices) : 0; -} +$amount = $block->getMsrpPriceCalculator()->getMsrpPriceValue($product); $msrpPrice = $block->renderAmount( $priceType->getCustomAmount($amount), 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/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/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/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/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/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/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/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/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php index 38ba0983514b0..72f166e8d07c1 100644 --- a/app/code/Magento/Paypal/Model/Express/Checkout.php +++ b/app/code/Magento/Paypal/Model/Express/Checkout.php @@ -1076,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/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml index 97c7fbc471e97..32c2fab40e97a 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml @@ -8,12 +8,15 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="OpenPayPalButtonCheckoutPage"> - <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn}}" stepKey="clickPayPalConfigureBtn"/> - <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab}}" stepKey="waitForAdvancedSettingTab"/> - <click selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab}}" stepKey="openAdvancedSettingTab"/> - <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.frontendExperienceSettingsTab}}" stepKey="waitForFrontendExperienceSettingsTab"/> - <click selector="{{PayPalAdvancedSettingConfigSection.frontendExperienceSettingsTab}}" stepKey="openFrontendExperienceSettingsTab"/> - <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.checkoutPageTab}}" stepKey="waitForCheckoutPageTab"/> - <click selector="{{PayPalAdvancedSettingConfigSection.checkoutPageTab}}" stepKey="openCheckoutPageTab"/> + <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 index 7bf26aceb316a..bae517ffe2f3e 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml @@ -8,18 +8,22 @@ <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}}" stepKey="clickPayPalConfigureBtn"/> - <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab}}" stepKey="waitForAdvancedSettingTab"/> - <fillField selector ="{{PayPalExpressCheckoutConfigSection.email}}" userInput="{{_CREDS.paypal_express_email}}" stepKey="inputEmailAssociatedWithPayPalMerchantAccount"/> - <selectOption selector ="{{PayPalExpressCheckoutConfigSection.apiMethod}}" userInput="API Signature" stepKey="inputAPIAuthenticationMethods"/> - <fillField selector ="{{PayPalExpressCheckoutConfigSection.username}}" userInput="{{_CREDS.paypal_express_api_username}}" stepKey="inputAPIUsername"/> - <fillField selector ="{{PayPalExpressCheckoutConfigSection.password}}" userInput="{{_CREDS.paypal_express_api_password}}" stepKey="inputAPIPassword"/> - <fillField selector ="{{PayPalExpressCheckoutConfigSection.signature}}" userInput="{{_CREDS.paypal_express_api_signature}}" stepKey="inputAPISignature"/> - <selectOption selector ="{{PayPalExpressCheckoutConfigSection.sandboxMode}}" userInput="Yes" stepKey="enableSandboxMode"/> - <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution}}" userInput="Yes" stepKey="enableSolution"/> - <fillField selector ="{{PayPalExpressCheckoutConfigSection.merchantID}}" userInput="{{_CREDS.paypal_express_merchantID}}" stepKey="inputMerchantID"/> + <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> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml index d97e60043cc5d..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"> @@ -89,4 +92,11 @@ <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 index 3ac0bb2707556..85f94cd8691a5 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml @@ -9,20 +9,24 @@ <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_us_paypal_alternative_payment_methods_express_checkout_us-head"/> - <element name="email" type="input" selector="#payment_us_paypal_alternative_payment_methods_express_checkout_us_express_checkout_required_express_checkout_required_express_checkout_business_account" /> - <element name="apiMethod" type="input" selector="#payment_us_paypal_alternative_payment_methods_express_checkout_us_express_checkout_required_express_checkout_required_express_checkout_api_authentication"/> - <element name="username" type="input" selector="#payment_us_paypal_alternative_payment_methods_express_checkout_us_express_checkout_required_express_checkout_required_express_checkout_api_username"/> - <element name="password" type="input" selector="#payment_us_paypal_alternative_payment_methods_express_checkout_us_express_checkout_required_express_checkout_required_express_checkout_api_password"/> - <element name="signature" type="input" selector="#payment_us_paypal_alternative_payment_methods_express_checkout_us_express_checkout_required_express_checkout_required_express_checkout_api_signature"/> - <element name="sandboxMode" type="input" selector="#payment_us_paypal_alternative_payment_methods_express_checkout_us_express_checkout_required_express_checkout_required_express_checkout_sandbox_flag"/> - <element name="enableSolution" type="input" selector="#payment_us_paypal_alternative_payment_methods_express_checkout_us_express_checkout_required_enable_express_checkout"/> - <element name="merchantID" type="input" selector="#payment_us_paypal_alternative_payment_methods_express_checkout_us_express_checkout_required_merchant_id"/> + <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_us_paypal_alternative_payment_methods_express_checkout_us_settings_ec_settings_ec_advanced-head"/> - <element name="frontendExperienceSettingsTab" type="button" selector="#payment_us_paypal_alternative_payment_methods_express_checkout_us_settings_ec_settings_ec_advanced_express_checkout_frontend-head"/> - <element name="checkoutPageTab" type="button" selector="#payment_us_paypal_alternative_payment_methods_express_checkout_us_settings_ec_settings_ec_advanced_express_checkout_frontend_checkout_page_button-head"/> + <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')]"/> 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/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/etc/adminhtml/rules/payment_gb.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml index 565962518881b..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> @@ -98,4 +98,4 @@ <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> -</rules> +</rules> \ No newline at end of file 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/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/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/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/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index 2303d2aa14f03..1b32866ed883c 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -75,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/Address/BillingAddressDataProvider.php b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/BillingAddressDataProvider.php deleted file mode 100644 index bcd781025a6d6..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/BillingAddressDataProvider.php +++ /dev/null @@ -1,72 +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\CartRepositoryInterface; -use Magento\Quote\Api\Data\AddressInterface; -use Magento\Quote\Api\Data\CartInterface; -use Magento\QuoteGraphQl\Model\Cart\Address\Mapper\Address; - -/** - * Collect and return information about a billing address - */ -class BillingAddressDataProvider -{ - /** - * @var CartRepositoryInterface - */ - private $cartRepository; - - /** - * @var Address - */ - private $addressMapper; - - /** - * @var ExtensibleDataObjectConverter - */ - private $dataObjectConverter; - - /** - * AddressDataProvider constructor. - * - * @param CartRepositoryInterface $cartRepository - * @param Address $addressMapper - * @param ExtensibleDataObjectConverter $dataObjectConverter - */ - public function __construct( - CartRepositoryInterface $cartRepository, - Address $addressMapper, - ExtensibleDataObjectConverter $dataObjectConverter - ) { - $this->cartRepository = $cartRepository; - $this->addressMapper = $addressMapper; - $this->dataObjectConverter = $dataObjectConverter; - } - - /** - * Collect and return information about a billing addresses - * - * @param CartInterface $cart - * @return null|array - */ - public function getCartAddresses(CartInterface $cart): ?array - { - $cart = $this->cartRepository->get($cart->getId()); - $billingAddress = $cart->getBillingAddress(); - - if (!$billingAddress) { - return null; - } - $billingData = $this->dataObjectConverter->toFlatArray($billingAddress, [], AddressInterface::class); - $addressData = array_merge($billingData, $this->addressMapper->toNestedArray($billingAddress)); - - return $addressData; - } -} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/Mapper/Address.php b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/Mapper/Address.php deleted file mode 100644 index 64ac407e2692a..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/Mapper/Address.php +++ /dev/null @@ -1,61 +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\Mapper; - -use Magento\Quote\Model\Quote\Address as QuoteAddress; - -/** - * Class Address - * - * Extract the necessary address fields from an Address model - */ -class Address -{ - /** - * Converts Address model data to nested array - * - * @param QuoteAddress $address - * @return array - */ - public function toNestedArray(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() - ]; - - 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/Address/ShippingAddressesDataProvider.php b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/ShippingAddressesDataProvider.php deleted file mode 100644 index eb9d760809594..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/ShippingAddressesDataProvider.php +++ /dev/null @@ -1,76 +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\CartRepositoryInterface; -use Magento\Quote\Api\Data\AddressInterface; -use Magento\Quote\Api\Data\CartInterface; -use Magento\QuoteGraphQl\Model\Cart\Address\Mapper\Address; - -/** - * Class AddressDataProvider - * - * Collect and return information about cart shipping and billing addresses - */ -class ShippingAddressesDataProvider -{ - /** - * @var ExtensibleDataObjectConverter - */ - private $dataObjectConverter; - - /** - * @var CartRepositoryInterface - */ - private $cartRepository; - - /** - * @var Address - */ - private $addressMapper; - - /** - * AddressDataProvider constructor. - * - * @param ExtensibleDataObjectConverter $dataObjectConverter - * @param CartRepositoryInterface $cartRepository - * @param Address $addressMapper - */ - public function __construct( - ExtensibleDataObjectConverter $dataObjectConverter, - CartRepositoryInterface $cartRepository, - Address $addressMapper - ) { - $this->dataObjectConverter = $dataObjectConverter; - $this->cartRepository = $cartRepository; - $this->addressMapper = $addressMapper; - } - - /** - * Collect and return information about shipping addresses - * - * @param CartInterface $cart - * @return array - */ - public function getCartAddresses(CartInterface $cart): array - { - $cart = $this->cartRepository->get($cart->getId()); - $addressData = []; - $shippingAddresses = $cart->getAllShippingAddresses(); - - if ($shippingAddresses) { - foreach ($shippingAddresses as $shippingAddress) { - $shippingData = $this->dataObjectConverter->toFlatArray($shippingAddress, [], AddressInterface::class); - $addressData[] = array_merge($shippingData, $this->addressMapper->toNestedArray($shippingAddress)); - } - } - - 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 index 97b1ed09decc4..cf277c729cdfd 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php @@ -7,14 +7,13 @@ namespace Magento\QuoteGraphQl\Model\Cart; -use Magento\Customer\Api\Data\AddressInterface; 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; -use Magento\Quote\Model\Quote\Address; -use Magento\Quote\Api\BillingAddressManagementInterface; -use Magento\Customer\Api\AddressRepositoryInterface; /** * Set billing address for a specified shopping cart @@ -22,78 +21,89 @@ class SetBillingAddressOnCart { /** - * @var BillingAddressManagementInterface + * @var QuoteAddressFactory */ - private $billingAddressManagement; + private $quoteAddressFactory; /** - * @var AddressRepositoryInterface + * @var CheckCustomerAccount */ - private $addressRepository; + private $checkCustomerAccount; /** - * @var Address + * @var GetCustomerAddress */ - private $addressModel; + private $getCustomerAddress; /** - * @var CheckCustomerAccount + * @var AssignBillingAddressToCart */ - private $checkCustomerAccount; + private $assignBillingAddressToCart; /** - * @param BillingAddressManagementInterface $billingAddressManagement - * @param AddressRepositoryInterface $addressRepository - * @param Address $addressModel + * @param QuoteAddressFactory $quoteAddressFactory * @param CheckCustomerAccount $checkCustomerAccount + * @param GetCustomerAddress $getCustomerAddress + * @param AssignBillingAddressToCart $assignBillingAddressToCart */ public function __construct( - BillingAddressManagementInterface $billingAddressManagement, - AddressRepositoryInterface $addressRepository, - Address $addressModel, - CheckCustomerAccount $checkCustomerAccount + QuoteAddressFactory $quoteAddressFactory, + CheckCustomerAccount $checkCustomerAccount, + GetCustomerAddress $getCustomerAddress, + AssignBillingAddressToCart $assignBillingAddressToCart ) { - $this->billingAddressManagement = $billingAddressManagement; - $this->addressRepository = $addressRepository; - $this->addressModel = $addressModel; + $this->quoteAddressFactory = $quoteAddressFactory; $this->checkCustomerAccount = $checkCustomerAccount; + $this->getCustomerAddress = $getCustomerAddress; + $this->assignBillingAddressToCart = $assignBillingAddressToCart; } /** - * @inheritdoc + * 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 $billingAddress): void + public function execute(ContextInterface $context, CartInterface $cart, array $billingAddressInput): void { - $customerAddressId = $billingAddress['customer_address_id'] ?? null; - $addressInput = $billingAddress['address'] ?? null; - $useForShipping = $billingAddress['use_for_shipping'] ?? false; + $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->addressModel->addData($addressInput); + $billingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); } else { $this->checkCustomerAccount->execute($context->getUserId(), $context->getUserType()); - - /** @var AddressInterface $customerAddress */ - $customerAddress = $this->addressRepository->getById($customerAddressId); - $billingAddress = $this->addressModel->importCustomerAddressData($customerAddress); + $customerAddress = $this->getCustomerAddress->execute((int)$customerAddressId, (int)$context->getUserId()); + $billingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress($customerAddress); } - $this->billingAddressManagement->assign($cart->getId(), $billingAddress, $useForShipping); + $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 index 7de5a4ac05c24..a03533ecefffa 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php @@ -11,7 +11,7 @@ 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\BillingAddressDataProvider; +use Magento\QuoteGraphQl\Model\Cart\ExtractDataFromAddress; /** * @inheritdoc @@ -19,17 +19,16 @@ class BillingAddress implements ResolverInterface { /** - * @var BillingAddressDataProvider + * @var ExtractDataFromAddress */ - private $addressDataProvider; + private $extractDataFromAddress; /** - * @param BillingAddressDataProvider $addressDataProvider + * @param ExtractDataFromAddress $extractDataFromAddress */ - public function __construct( - BillingAddressDataProvider $addressDataProvider - ) { - $this->addressDataProvider = $addressDataProvider; + public function __construct(ExtractDataFromAddress $extractDataFromAddress) + { + $this->extractDataFromAddress = $extractDataFromAddress; } /** @@ -40,9 +39,14 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (!isset($value['model'])) { throw new LocalizedException(__('"model" value should be specified')); } - $cart = $value['model']; - return $this->addressDataProvider->getCartAddresses($cart); + $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 index 01a35f4b4152f..f7c9a4b0697b8 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php @@ -11,13 +11,10 @@ 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\GetCartForUser; use Magento\QuoteGraphQl\Model\Cart\SetBillingAddressOnCart as SetBillingAddressOnCartModel; /** - * Class SetBillingAddressOnCart - * * Mutation resolver for setting billing address for shopping cart */ class SetBillingAddressOnCart implements ResolverInterface @@ -27,11 +24,6 @@ class SetBillingAddressOnCart implements ResolverInterface */ private $getCartForUser; - /** - * @var ArrayManager - */ - private $arrayManager; - /** * @var SetBillingAddressOnCartModel */ @@ -39,16 +31,13 @@ class SetBillingAddressOnCart implements ResolverInterface /** * @param GetCartForUser $getCartForUser - * @param ArrayManager $arrayManager * @param SetBillingAddressOnCartModel $setBillingAddressOnCart */ public function __construct( GetCartForUser $getCartForUser, - ArrayManager $arrayManager, SetBillingAddressOnCartModel $setBillingAddressOnCart ) { $this->getCartForUser = $getCartForUser; - $this->arrayManager = $arrayManager; $this->setBillingAddressOnCart = $setBillingAddressOnCart; } @@ -57,26 +46,23 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - $billingAddress = $this->arrayManager->get('input/billing_address', $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 (!$billingAddress) { + $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']; - $maskedCartId = $args['input']['cart_id']; $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); - $this->setBillingAddressOnCart->execute($context, $cart, $billingAddress); return [ 'cart' => [ - 'cart_id' => $maskedCartId, '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 index caa0eee22d702..3a55ef9ae25a8 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php @@ -11,7 +11,7 @@ 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\ShippingAddressesDataProvider; +use Magento\QuoteGraphQl\Model\Cart\ExtractDataFromAddress; /** * @inheritdoc @@ -19,17 +19,16 @@ class ShippingAddresses implements ResolverInterface { /** - * @var ShippingAddressesDataProvider + * @var ExtractDataFromAddress */ - private $addressDataProvider; + private $extractDataFromAddress; /** - * @param ShippingAddressesDataProvider $addressDataProvider + * @param ExtractDataFromAddress $extractDataFromAddress */ - public function __construct( - ShippingAddressesDataProvider $addressDataProvider - ) { - $this->addressDataProvider = $addressDataProvider; + public function __construct(ExtractDataFromAddress $extractDataFromAddress) + { + $this->extractDataFromAddress = $extractDataFromAddress; } /** @@ -40,9 +39,16 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (!isset($value['model'])) { throw new LocalizedException(__('"model" value should be specified')); } - $cart = $value['model']; - return $this->addressDataProvider->getCartAddresses($cart); + $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 3448d640618d4..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") + 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,6 +64,21 @@ 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! @@ -59,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 { @@ -98,14 +145,16 @@ type ApplyCouponToCartOutput { } type Cart { - cart_id: String - items: [CartItemInterface] - applied_coupon: AppliedCoupon + 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 @@ -116,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! } @@ -138,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 { @@ -163,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! } @@ -186,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") { @@ -219,8 +285,3 @@ type CartItemSelectedOptionValuePrice { units: String! type: PriceTypeEnum! } - -input CartItemDetailsInput { - sku: String! - qty: Float! -} 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/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/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/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/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/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/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 f3e926f109c35..48deddb2fe5ac 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -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/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/Service/InvoiceService.php b/app/code/Magento/Sales/Model/Service/InvoiceService.php index ba6ae7eb14ba7..02242e92c8bf5 100644 --- a/app/code/Magento/Sales/Model/Service/InvoiceService.php +++ b/app/code/Magento/Sales/Model/Service/InvoiceService.php @@ -190,6 +190,11 @@ private function prepareItemsQty(Order $order, array $qtys = []) 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; } } 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/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/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/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/ResourceModel/Rule/Collection.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php index 5e6f3847c8e31..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. @@ -107,12 +110,15 @@ protected function mapAssociatedEntities($entityType, $objectField) $associatedEntities = $this->getConnection()->fetchAll($select); - array_map(function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { - $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); - $itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField); - $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; - $item->setData($objectField, $itemAssociatedValue); - }, $associatedEntities); + array_map( + function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { + $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); + $itemAssociatedValue = $item->getData($objectField) ?? []; + $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; + $item->setData($objectField, $itemAssociatedValue); + }, + $associatedEntities + ); } /** @@ -141,6 +147,7 @@ protected function _afterLoad() * @param string $couponCode * @param string|null $now * @param Address $address allow extensions to further filter out rules based on quote address + * @throws \Zend_Db_Select_Exception * @use $this->addWebsiteGroupDateFilter() * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return $this @@ -153,32 +160,24 @@ public function setValidationFilter( Address $address = null ) { if (!$this->getFlag('validation_filter')) { - /* We need to overwrite joinLeft if coupon is applied */ - $this->getSelect()->reset(); - parent::_initSelect(); + $this->prepareSelect($websiteId, $customerGroupId, $now); - $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); - $select = $this->getSelect(); + $noCouponRules = $this->getNoCouponCodeSelect(); - $connection = $this->getConnection(); - if (strlen($couponCode)) { - $noCouponWhereCondition = $connection->quoteInto( - 'main_table.coupon_type = ?', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); - $relatedRulesIds = $this->getCouponRelatedRuleIds($couponCode); - - $select->where( - $noCouponWhereCondition . ' OR main_table.rule_id IN (?)', - $relatedRulesIds, - Select::TYPE_CONDITION - ); + if ($couponCode) { + $couponRules = $this->getCouponCodeSelect($couponCode); + + $allAllowedRules = $this->getConnection()->select(); + $allAllowedRules->union([$noCouponRules, $couponRules], Select::SQL_UNION_ALL); + + $wrapper = $this->getConnection()->select(); + $wrapper->from($allAllowedRules); + + $this->_select = $wrapper; } else { - $this->addFieldToFilter( - 'main_table.coupon_type', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); + $this->_select = $noCouponRules; } + $this->setOrder('sort_order', self::SORT_ORDER_ASC); $this->setFlag('validation_filter', true); } @@ -187,72 +186,98 @@ public function setValidationFilter( } /** - * Get rules ids related to coupon code + * Recreate the default select object for specific needs of salesrule evaluation with coupon codes. * - * @param string $couponCode - * @return array + * @param int $websiteId + * @param int $customerGroupId + * @param string $now */ - private function getCouponRelatedRuleIds(string $couponCode): array + private function prepareSelect($websiteId, $customerGroupId, $now) { - $connection = $this->getConnection(); - $select = $connection->select()->from( - ['main_table' => $this->getTable('salesrule')], - 'rule_id' + $this->getSelect()->reset(); + parent::_initSelect(); + + $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); + } + + /** + * Return select object to determine all active rules not needing a coupon code. + * + * @return Select + */ + private function getNoCouponCodeSelect() + { + $noCouponSelect = clone $this->getSelect(); + + $noCouponSelect->where( + 'main_table.coupon_type = ?', + Rule::COUPON_TYPE_NO_COUPON ); - $select->joinLeft( - ['rule_coupons' => $this->getTable('salesrule_coupon')], - $connection->quoteInto( - 'main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != ?', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, - null - ) + + $noCouponSelect->columns([Coupon::KEY_CODE => new \Zend_Db_Expr('NULL')]); + + return $noCouponSelect; + } + + /** + * Determine all active rules that are valid for the given coupon code. + * + * @param string $couponCode + * @return Select + */ + private function getCouponCodeSelect($couponCode) + { + $couponSelect = clone $this->getSelect(); + + $this->joinCouponTable($couponCode, $couponSelect); + + $notExpired = $this->getConnection()->quoteInto( + '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', + $this->_date->date()->format('Y-m-d') ); - $autoGeneratedCouponCondition = [ - $connection->quoteInto( - "main_table.coupon_type = ?", - \Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO - ), - $connection->quoteInto( - "rule_coupons.type = ?", - \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED - ), - ]; - - $orWhereConditions = [ - "(" . implode($autoGeneratedCouponCondition, " AND ") . ")", - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - ]; - - $andWhereConditions = [ - $connection->quoteInto( - 'rule_coupons.code = ?', - $couponCode - ), - $connection->quoteInto( - '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', - $this->_date->date()->format('Y-m-d') - ), - ]; - - $orWhereCondition = implode(' OR ', $orWhereConditions); - $andWhereCondition = implode(' AND ', $andWhereConditions); - - $select->where( - '(' . $orWhereCondition . ') AND ' . $andWhereCondition, + $isAutogenerated = + $this->getConnection()->quoteInto('main_table.coupon_type = ?', Rule::COUPON_TYPE_AUTO) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.type = ?', CouponInterface::TYPE_GENERATED); + + $isValidSpecific = + $this->getConnection()->quoteInto('(main_table.coupon_type = ?)', Rule::COUPON_TYPE_SPECIFIC) + . ' AND (' . + '(main_table.use_auto_generation = 1 AND rule_coupons.type = 1)' + . ' OR ' . + '(main_table.use_auto_generation = 0 AND rule_coupons.type = 0)' + . ')'; + + $couponSelect->where( + "$notExpired AND ($isAutogenerated OR $isValidSpecific)", null, Select::TYPE_CONDITION ); - $select->group('main_table.rule_id'); - return $connection->fetchCol($select); + return $couponSelect; + } + + /** + * Join 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] + ); } /** 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/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index 521734ab9f292..8f6e63534b0ca 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -189,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/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/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/ResourceModel/SynonymReader.php b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php index 45eee0a4001d1..46e794a1954cf 100644 --- a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php +++ b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php @@ -87,7 +87,7 @@ private function queryByPhrase($phrase) { $matchQuery = $this->fullTextSelect->getMatchQuery( ['synonyms' => 'synonyms'], - $this->escapePhrase($phrase), + $phrase, Fulltext::FULLTEXT_MODE_BOOLEAN ); $query = $this->getConnection()->select()->from( @@ -97,18 +97,6 @@ private function queryByPhrase($phrase) return $this->getConnection()->fetchAll($query); } - /** - * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. - * - * @see https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html - * @param string $phrase - * @return string - */ - private function escapePhrase(string $phrase): string - { - return preg_replace('/@+|[@+-]+$/', '', $phrase); - } - /** * A private helper function to retrieve matching synonym groups per scope * diff --git a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml index 9e04bbb12a796..9e5bde9a2be49 100644 --- a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml @@ -14,6 +14,6 @@ <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="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/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/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/Section/AdminStoresGridSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml index b02e9adaed45e..592af42f2de30 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml @@ -21,5 +21,7 @@ <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/StorefrontHeaderSection.xml b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml index 8004b750a4d1f..416808e58e6b3 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -13,5 +13,6 @@ <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/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/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/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 bd611d0cc1863..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 @@ -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; @@ -924,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'); @@ -941,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) { 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/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/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/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/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/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/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/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/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/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/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/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/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php index 2ec573b6459da..e1bb094e7fc39 100644 --- a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php +++ b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php @@ -81,7 +81,12 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s $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(); } 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 7c21acdf943ba..7b789845fe249 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml @@ -19,5 +19,8 @@ <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/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/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/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/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/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_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_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/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/_messages.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less index 4621349eb2803..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: -.9rem; + margin-top: -1.1rem; } } 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 4b77c518d3df8..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; @@ -697,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/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/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 d4a81027cc3d7..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%; } } } 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/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_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/checkout/_order-summary.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less index 308b034026e18..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 @@ -152,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 { @@ -231,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 ec6762de3d46d..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%; } } } diff --git a/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less index 088372808aa6a..fe49d6679a613 100644 --- a/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less @@ -133,9 +133,18 @@ } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { - .table-wrapper.grouped { - .lib-css(margin-left, -@layout__width-xs-indent); - .lib-css(margin-right, -@layout__width-xs-indent); + .product-add-form { + .table-wrapper.grouped { + .lib-css(margin-left, -@layout__width-xs-indent); + .lib-css(margin-right, -@layout__width-xs-indent); + .table.data.grouped { + tr { + td { + padding: 5px 10px 5px 15px; + } + } + } + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less index 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_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 ab2eb1764f781..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 @@ -561,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; } } } @@ -693,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_Theme/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less index 99da7716f9274..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 // --------------------------------------------- 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 2db14c05ccad7..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 @@ -395,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/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 7120fe77b0e55..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", @@ -149,6 +149,7 @@ "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": "*", @@ -180,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": "*", @@ -236,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 48aef8b603d4a..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": "f22c780b1ed27ba951c0562e20078a70", + "content-hash": "60007664938710edf52eadddd7551867", "packages": [ { "name": "braintree/braintree_php", @@ -257,16 +257,16 @@ }, { "name": "composer/composer", - "version": "1.8.3", + "version": "1.8.4", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "a6a3b44581398b7135c7baa0557b7c5b10808b47" + "reference": "bc364c2480c17941e2135cfc568fa41794392534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/a6a3b44581398b7135c7baa0557b7c5b10808b47", - "reference": "a6a3b44581398b7135c7baa0557b7c5b10808b47", + "url": "https://api.github.com/repos/composer/composer/zipball/bc364c2480c17941e2135cfc568fa41794392534", + "reference": "bc364c2480c17941e2135cfc568fa41794392534", "shasum": "" }, "require": { @@ -333,7 +333,7 @@ "dependency", "package" ], - "time": "2019-01-30T07:31:34+00:00" + "time": "2019-02-11T09:52:10+00:00" }, { "name": "composer/semver", @@ -535,29 +535,31 @@ }, { "name": "elasticsearch/elasticsearch", - "version": "v5.4.0", + "version": "v6.1.0", "source": { "type": "git", "url": "https://github.com/elastic/elasticsearch-php.git", - "reference": "d3c5b55ad94f5053ca76c48585b4cde2cdc6bc59" + "reference": "b237a37b2cdf23a5a17fd3576cdea771394ad00d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/d3c5b55ad94f5053ca76c48585b4cde2cdc6bc59", - "reference": "d3c5b55ad94f5053ca76c48585b4cde2cdc6bc59", + "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": "2019-01-08T18:57:00+00:00" + "time": "2019-01-08T18:53:46+00:00" }, { "name": "guzzlehttp/ringphp", @@ -1910,7 +1912,7 @@ }, { "name": "symfony/css-selector", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -2026,16 +2028,16 @@ }, { "name": "symfony/filesystem", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7c16ebc2629827d4ec915a52ac809768d060a4ee" + "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7c16ebc2629827d4ec915a52ac809768d060a4ee", - "reference": "7c16ebc2629827d4ec915a52ac809768d060a4ee", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e16b9e471703b2c60b95f14d31c1239f68f11601", + "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601", "shasum": "" }, "require": { @@ -2072,20 +2074,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2019-01-16T20:35:37+00:00" + "time": "2019-02-07T11:40:08+00:00" }, { "name": "symfony/finder", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c" + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ef71816cbb264988bb57fe6a73f610888b9aa70c", - "reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c", + "url": "https://api.github.com/repos/symfony/finder/zipball/267b7002c1b70ea80db0833c3afe05f0fbde580a", + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a", "shasum": "" }, "require": { @@ -2121,7 +2123,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2019-01-16T20:35:37+00:00" + "time": "2019-02-23T15:42:05+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2751,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": { @@ -2771,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": { @@ -2805,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", @@ -4375,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": { @@ -4399,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": { @@ -4418,7 +4420,7 @@ "uri", "zf" ], - "time": "2018-04-30T13:40:08+00:00" + "time": "2019-02-27T21:39:04+00:00" }, { "name": "zendframework/zend-validator", @@ -4887,16 +4889,16 @@ }, { "name": "codeception/phpunit-wrapper", - "version": "6.5.1", + "version": "6.6.1", "source": { "type": "git", "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "d78f9eb9c4300a5924cc27dee03e8c1a96fcf5f3" + "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/d78f9eb9c4300a5924cc27dee03e8c1a96fcf5f3", - "reference": "d78f9eb9c4300a5924cc27dee03e8c1a96fcf5f3", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", + "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", "shasum": "" }, "require": { @@ -4910,7 +4912,7 @@ }, "require-dev": { "codeception/specify": "*", - "vlucas/phpdotenv": "^2.4" + "vlucas/phpdotenv": "^3.0" }, "type": "library", "autoload": { @@ -4929,24 +4931,24 @@ } ], "description": "PHPUnit classes used by Codeception", - "time": "2019-01-13T10:34:55+00:00" + "time": "2019-02-26T20:47:39+00:00" }, { "name": "codeception/stub", - "version": "2.0.4", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e" + "reference": "853657f988942f7afb69becf3fd0059f192c705a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/f50bc271f392a2836ff80690ce0c058efe1ae03e", - "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/853657f988942f7afb69becf3fd0059f192c705a", + "reference": "853657f988942f7afb69becf3fd0059f192c705a", "shasum": "" }, "require": { - "phpunit/phpunit": ">=4.8 <8.0" + "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3" }, "type": "library", "autoload": { @@ -4959,7 +4961,7 @@ "MIT" ], "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2018-07-26T11:55:37+00:00" + "time": "2019-03-02T15:35:10+00:00" }, { "name": "consolidation/annotated-command", @@ -5059,16 +5061,16 @@ }, { "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": { @@ -5077,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" @@ -5089,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" } @@ -5109,7 +5138,7 @@ } ], "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", @@ -5259,16 +5288,16 @@ }, { "name": "consolidation/robo", - "version": "1.4.4", + "version": "1.4.6", "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "8bec6a6ea54a7d03d56552a4250c49dec3b3083d" + "reference": "d4805a1abbc730e9a6d64ede2eba56f91a2b4eb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/8bec6a6ea54a7d03d56552a4250c49dec3b3083d", - "reference": "8bec6a6ea54a7d03d56552a4250c49dec3b3083d", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/d4805a1abbc730e9a6d64ede2eba56f91a2b4eb3", + "reference": "d4805a1abbc730e9a6d64ede2eba56f91a2b4eb3", "shasum": "" }, "require": { @@ -5363,7 +5392,7 @@ } ], "description": "Modern task runner", - "time": "2019-02-08T20:59:23+00:00" + "time": "2019-02-17T05:32:27+00:00" }, { "name": "consolidation/self-update", @@ -6591,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": { @@ -6660,7 +6689,7 @@ "magento", "testing" ], - "time": "2019-01-29T15:31:14+00:00" + "time": "2019-02-19T16:03:22+00:00" }, { "name": "mikey179/vfsStream", @@ -7813,6 +7842,7 @@ "mock", "xunit" ], + "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, { @@ -8556,16 +8586,16 @@ }, { "name": "symfony/browser-kit", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "ee4462581eb54bf34b746e4a5d522a4f21620160" + "reference": "61d85c5af2fc058014c7c89504c3944e73a086f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/ee4462581eb54bf34b746e4a5d522a4f21620160", - "reference": "ee4462581eb54bf34b746e4a5d522a4f21620160", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/61d85c5af2fc058014c7c89504c3944e73a086f0", + "reference": "61d85c5af2fc058014c7c89504c3944e73a086f0", "shasum": "" }, "require": { @@ -8609,20 +8639,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2019-01-16T21:31:25+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/config", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "25a2e7abe0d97e70282537292e3df45cf6da7b98" + "reference": "7f70d79c7a24a94f8e98abb988049403a53d7b31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/25a2e7abe0d97e70282537292e3df45cf6da7b98", - "reference": "25a2e7abe0d97e70282537292e3df45cf6da7b98", + "url": "https://api.github.com/repos/symfony/config/zipball/7f70d79c7a24a94f8e98abb988049403a53d7b31", + "reference": "7f70d79c7a24a94f8e98abb988049403a53d7b31", "shasum": "" }, "require": { @@ -8672,7 +8702,7 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2019-01-30T11:44:30+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/contracts", @@ -8744,16 +8774,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "72c14cbc0c27706b9b4c33b9cd7a280972ff4806" + "reference": "cdadb3765df7c89ac93628743913b92bb91f1704" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/72c14cbc0c27706b9b4c33b9cd7a280972ff4806", - "reference": "72c14cbc0c27706b9b4c33b9cd7a280972ff4806", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/cdadb3765df7c89ac93628743913b92bb91f1704", + "reference": "cdadb3765df7c89ac93628743913b92bb91f1704", "shasum": "" }, "require": { @@ -8813,20 +8843,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2019-01-30T17:51:38+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/dom-crawler", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "d8476760b04cdf7b499c8718aa437c20a9155103" + "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d8476760b04cdf7b499c8718aa437c20a9155103", - "reference": "d8476760b04cdf7b499c8718aa437c20a9155103", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/53c97769814c80a84a8403efcf3ae7ae966d53bb", + "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb", "shasum": "" }, "require": { @@ -8870,20 +8900,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2019-01-16T20:35:37+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "8d2318b73e0a1bc75baa699d00ebe2ae8b595a39" + "reference": "850a667d6254ccf6c61d853407b16f21c4579c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8d2318b73e0a1bc75baa699d00ebe2ae8b595a39", - "reference": "8d2318b73e0a1bc75baa699d00ebe2ae8b595a39", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/850a667d6254ccf6c61d853407b16f21c4579c77", + "reference": "850a667d6254ccf6c61d853407b16f21c4579c77", "shasum": "" }, "require": { @@ -8924,20 +8954,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2019-01-29T09:49:29+00:00" + "time": "2019-02-26T08:03:39+00:00" }, { "name": "symfony/options-resolver", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "831b272963a8aa5a0613a1a7f013322d8161bbbb" + "reference": "3896e5a7d06fd15fa4947694c8dcdd371ff147d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/831b272963a8aa5a0613a1a7f013322d8161bbbb", - "reference": "831b272963a8aa5a0613a1a7f013322d8161bbbb", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/3896e5a7d06fd15fa4947694c8dcdd371ff147d1", + "reference": "3896e5a7d06fd15fa4947694c8dcdd371ff147d1", "shasum": "" }, "require": { @@ -8978,7 +9008,7 @@ "configuration", "options" ], - "time": "2019-01-16T21:31:25+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/polyfill-php70", @@ -9096,7 +9126,7 @@ }, { "name": "symfony/stopwatch", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -9146,16 +9176,16 @@ }, { "name": "symfony/yaml", - "version": "v3.4.22", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "ba11776e9e6c15ad5759a07bffb15899bac75c2d" + "reference": "57f1ce82c997f5a8701b89ef970e36bb657fd09c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ba11776e9e6c15ad5759a07bffb15899bac75c2d", - "reference": "ba11776e9e6c15ad5759a07bffb15899bac75c2d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/57f1ce82c997f5a8701b89ef970e36bb657fd09c", + "reference": "57f1ce82c997f5a8701b89ef970e36bb657fd09c", "shasum": "" }, "require": { @@ -9201,7 +9231,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2019-01-16T10:59:17+00:00" + "time": "2019-02-23T15:06:07+00:00" }, { "name": "theseer/fdomdocument", 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 5d2c2c5eb1769..0000000000000 --- a/dev/tests/acceptance/composer.lock +++ /dev/null @@ -1,3534 +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#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "b93d599d375af66b29edfd8a35875e69", - "packages": [ - { - "name": "behat/gherkin", - "version": "v4.6.0", - "source": { - "type": "git", - "url": "https://github.com/Behat/Gherkin.git", - "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/ab0a02ea14893860bca00f225f5621d351a3ad07", - "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07", - "shasum": "" - }, - "require": { - "php": ">=5.3.1" - }, - "require-dev": { - "phpunit/phpunit": "~4.5|~5", - "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" - }, - "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": "2019-01-16T14:22:17+00:00" - }, - { - "name": "codeception/codeception", - "version": "2.4.5", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Codeception.git", - "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5fee32d5c82791548931cbc34806b4de6aa1abfc", - "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc", - "shasum": "" - }, - "require": { - "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.6.0 <8.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-08-01T07:21:49+00:00" - }, - { - "name": "codeception/phpunit-wrapper", - "version": "7.6.1", - "source": { - "type": "git", - "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "ed4b12beb167dc2ecea293b4f6df6c20ce8d280f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/ed4b12beb167dc2ecea293b4f6df6c20ce8d280f", - "reference": "ed4b12beb167dc2ecea293b4f6df6c20ce8d280f", - "shasum": "" - }, - "require": { - "phpunit/php-code-coverage": "^6.0", - "phpunit/phpunit": ">=7.1 <7.6", - "sebastian/comparator": "^3.0", - "sebastian/diff": "^3.0" - }, - "require-dev": { - "codeception/specify": "*", - "vlucas/phpdotenv": "^2.4" - }, - "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-01-13T10:34:39+00:00" - }, - { - "name": "codeception/stub", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Stub.git", - "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/f50bc271f392a2836ff80690ce0c058efe1ae03e", - "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e", - "shasum": "" - }, - "require": { - "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-07-26T11:55:37+00:00" - }, - { - "name": "consolidation/annotated-command", - "version": "2.11.2", - "source": { - "type": "git", - "url": "https://github.com/consolidation/annotated-command.git", - "reference": "004af26391cd7d1cd04b0ac736dc1324d1b4f572" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/004af26391cd7d1cd04b0ac736dc1324d1b4f572", - "reference": "004af26391cd7d1cd04b0ac736dc1324d1b4f572", - "shasum": "" - }, - "require": { - "consolidation/output-formatters": "^3.4", - "php": ">=5.4.5", - "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": "^3", - "php-coveralls/php-coveralls": "^1", - "phpunit/phpunit": "^6", - "squizlabs/php_codesniffer": "^2.7" - }, - "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" - } - }, - "scenario-options": { - "create-lockfile": "false" - } - }, - "phpunit4": { - "require-dev": { - "phpunit/phpunit": "^4.8.36" - }, - "remove": [ - "php-coveralls/php-coveralls" - ], - "config": { - "platform": { - "php": "5.4.8" - } - } - } - }, - "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": "2019-02-02T02:29:53+00:00" - }, - { - "name": "consolidation/config", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/consolidation/config.git", - "reference": "925231dfff32f05b787e1fddb265e789b939cf4c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/config/zipball/925231dfff32f05b787e1fddb265e789b939cf4c", - "reference": "925231dfff32f05b787e1fddb265e789b939cf4c", - "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": "^5", - "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-10-24T17:55:35+00:00" - }, - { - "name": "consolidation/log", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/consolidation/log.git", - "reference": "b2e887325ee90abc96b0a8b7b474cd9e7c896e3a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/log/zipball/b2e887325ee90abc96b0a8b7b474cd9e7c896e3a", - "reference": "b2e887325ee90abc96b0a8b7b474cd9e7c896e3a", - "shasum": "" - }, - "require": { - "php": ">=5.4.5", - "psr/log": "^1.0", - "symfony/console": "^2.8|^3|^4" - }, - "require-dev": { - "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" - } - }, - "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": "2019-01-01T17:30:51+00:00" - }, - { - "name": "consolidation/output-formatters", - "version": "3.4.0", - "source": { - "type": "git", - "url": "https://github.com/consolidation/output-formatters.git", - "reference": "a942680232094c4a5b21c0b7e54c20cce623ae19" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/a942680232094c4a5b21c0b7e54c20cce623ae19", - "reference": "a942680232094c4a5b21c0b7e54c20cce623ae19", - "shasum": "" - }, - "require": { - "dflydev/dot-access-data": "^1.1.0", - "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-10-19T22:35:38+00:00" - }, - { - "name": "consolidation/robo", - "version": "1.4.4", - "source": { - "type": "git", - "url": "https://github.com/consolidation/Robo.git", - "reference": "8bec6a6ea54a7d03d56552a4250c49dec3b3083d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/8bec6a6ea54a7d03d56552a4250c49dec3b3083d", - "reference": "8bec6a6ea54a7d03d56552a4250c49dec3b3083d", - "shasum": "" - }, - "require": { - "consolidation/annotated-command": "^2.10.2", - "consolidation/config": "^1.0.10", - "consolidation/log": "~1", - "consolidation/output-formatters": "^3.1.13", - "consolidation/self-update": "^1", - "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": "^3", - "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.4", - "php-coveralls/php-coveralls": "^1", - "phpunit/php-code-coverage": "~2|~4", - "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": { - "scenarios": { - "symfony4": { - "require": { - "symfony/console": "^4" - }, - "config": { - "platform": { - "php": "7.1.3" - } - } - }, - "symfony2": { - "require": { - "symfony/console": "^2.8" - }, - "remove": [ - "goaop/framework" - ], - "config": { - "platform": { - "php": "5.5.9" - } - }, - "scenario-options": { - "create-lockfile": "false" - } - } - }, - "branch-alias": { - "dev-master": "2.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": "2019-02-08T20:59:23+00:00" - }, - { - "name": "consolidation/self-update", - "version": "1.1.5", - "source": { - "type": "git", - "url": "https://github.com/consolidation/self-update.git", - "reference": "a1c273b14ce334789825a09d06d4c87c0a02ad54" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/self-update/zipball/a1c273b14ce334789825a09d06d4c87c0a02ad54", - "reference": "a1c273b14ce334789825a09d06d4c87c0a02ad54", - "shasum": "" - }, - "require": { - "php": ">=5.5.0", - "symfony/console": "^2.8|^3|^4", - "symfony/filesystem": "^2.5|^3|^4" - }, - "bin": [ - "scripts/release" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "SelfUpdate\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - }, - { - "name": "Alexander Menk", - "email": "menk@mestrona.net" - } - ], - "description": "Provides a self:update command for Symfony Console applications.", - "time": "2018-10-28T01:52:03+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.5.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "9f83dded91781a01c63574e387eaa769be769115" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", - "reference": "9f83dded91781a01c63574e387eaa769be769115", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0", - "ralouphie/getallheaders": "^2.0.5" - }, - "provide": { - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5-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", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" - ], - "time": "2018-12-04T20:46:45+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.3", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-phar": "*", - "phar-io/version": "^2.0", - "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": "2018-07-08T19:23:20+00:00" - }, - { - "name": "phar-io/version", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", - "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": "2018-07-08T19:19:57+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.8.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", - "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", - "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 || ^7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8.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-08-05T17:53:17+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "6.1.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", - "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": "^7.1", - "phpunit/php-file-iterator": "^2.0", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.0", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.1 || ^4.0", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" - }, - "suggest": { - "ext-xdebug": "^2.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.1-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-10-31T16:06:48+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "050bedf145a257b1ff02746c31894800e5122946" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", - "reference": "050bedf145a257b1ff02746c31894800e5122946", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.1" - }, - "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": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2018-09-13T20:33:42+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": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b8454ea6958c3dee38453d3bd571e023108c91f", - "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" - }, - "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", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2018-02-01T13:07:23+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c99e3be9d3e85f60646f152f9002d46ed7770d18", - "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.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": "2018-10-30T05:52:18+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "7.5.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "2896657da5fb237bc316bdfc18c2650efeee0dc0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2896657da5fb237bc316bdfc18c2650efeee0dc0", - "reference": "2896657da5fb237bc316bdfc18c2650efeee0dc0", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.1", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "myclabs/deep-copy": "^1.7", - "phar-io/manifest": "^1.0.2", - "phar-io/version": "^2.0", - "php": "^7.1", - "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^6.0.7", - "phpunit/php-file-iterator": "^2.0.1", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^2.0", - "sebastian/comparator": "^3.0", - "sebastian/diff": "^3.0", - "sebastian/environment": "^4.0", - "sebastian/exporter": "^3.1", - "sebastian/global-state": "^2.0", - "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^2.0", - "sebastian/version": "^2.0.1" - }, - "conflict": { - "phpunit/phpunit-mock-objects": "*" - }, - "require-dev": { - "ext-pdo": "*" - }, - "suggest": { - "ext-soap": "*", - "ext-xdebug": "*", - "phpunit/php-invoker": "^2.0" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.5-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": "2019-02-07T14:15:04+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.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", - "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", - "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": "2018-11-20T15:27:04+00:00" - }, - { - "name": "ralouphie/getallheaders", - "version": "2.0.5", - "source": { - "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", - "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", - "shasum": "" - }, - "require": { - "php": ">=5.3" - }, - "require-dev": { - "phpunit/phpunit": "~3.7.0", - "satooshi/php-coveralls": ">=1.0" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "time": "2016-02-11T07:05:27+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": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", - "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", - "shasum": "" - }, - "require": { - "php": "^7.1", - "sebastian/diff": "^3.0", - "sebastian/exporter": "^3.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-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-07-12T15:12:46+00:00" - }, - { - "name": "sebastian/diff", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.0", - "symfony/process": "^2 || ^3.3 || ^4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.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", - "udiff", - "unidiff", - "unified diff" - ], - "time": "2019-02-04T06:01:07+00:00" - }, - { - "name": "sebastian/environment", - "version": "4.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "6fda8ce1974b62b14935adc02a9ed38252eca656" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6fda8ce1974b62b14935adc02a9ed38252eca656", - "reference": "6fda8ce1974b62b14935adc02a9ed38252eca656", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.5" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-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": "2019-02-01T05:27:49+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": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", - "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "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": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2018-10-04T04:07:39+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.2.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/browser-kit.git", - "reference": "ee4462581eb54bf34b746e4a5d522a4f21620160" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/ee4462581eb54bf34b746e4a5d522a4f21620160", - "reference": "ee4462581eb54bf34b746e4a5d522a4f21620160", - "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.2-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": "2019-01-16T21:31:25+00:00" - }, - { - "name": "symfony/console", - "version": "v4.2.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4", - "reference": "1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/contracts": "^1.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "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", - "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": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-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": "2019-01-25T14:35:16+00:00" - }, - { - "name": "symfony/contracts", - "version": "v1.0.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/contracts.git", - "reference": "1aa7ab2429c3d594dd70689604b5cf7421254cdf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/contracts/zipball/1aa7ab2429c3d594dd70689604b5cf7421254cdf", - "reference": "1aa7ab2429c3d594dd70689604b5cf7421254cdf", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "require-dev": { - "psr/cache": "^1.0", - "psr/container": "^1.0" - }, - "suggest": { - "psr/cache": "When using the Cache contracts", - "psr/container": "When using the Service contracts", - "symfony/cache-contracts-implementation": "", - "symfony/service-contracts-implementation": "", - "symfony/translation-contracts-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\": "" - }, - "exclude-from-classmap": [ - "**/Tests/" - ] - }, - "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": "A set of abstractions extracted out of the Symfony components", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "time": "2018-12-05T08:06:11+00:00" - }, - { - "name": "symfony/css-selector", - "version": "v4.2.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "48eddf66950fa57996e1be4a55916d65c10c604a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/48eddf66950fa57996e1be4a55916d65c10c604a", - "reference": "48eddf66950fa57996e1be4a55916d65c10c604a", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-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": "2019-01-16T20:31:39+00:00" - }, - { - "name": "symfony/dom-crawler", - "version": "v4.2.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "d8476760b04cdf7b499c8718aa437c20a9155103" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d8476760b04cdf7b499c8718aa437c20a9155103", - "reference": "d8476760b04cdf7b499c8718aa437c20a9155103", - "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.2-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": "2019-01-16T20:35:37+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v4.2.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "bd09ad265cd50b2b9d09d65ce6aba2d29bc81fe1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/bd09ad265cd50b2b9d09d65ce6aba2d29bc81fe1", - "reference": "bd09ad265cd50b2b9d09d65ce6aba2d29bc81fe1", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/contracts": "^1.0" - }, - "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.2-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": "2019-01-16T20:35:37+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v4.2.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "7c16ebc2629827d4ec915a52ac809768d060a4ee" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7c16ebc2629827d4ec915a52ac809768d060a4ee", - "reference": "7c16ebc2629827d4ec915a52ac809768d060a4ee", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-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": "2019-01-16T20:35:37+00:00" - }, - { - "name": "symfony/finder", - "version": "v4.2.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ef71816cbb264988bb57fe6a73f610888b9aa70c", - "reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-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": "2019-01-16T20:35:37+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.10.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.9-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-08-06T14:22:27+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.10.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", - "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.9-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-09-21T13:07:52+00:00" - }, - { - "name": "symfony/process", - "version": "v4.2.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "6c05edb11fbeff9e2b324b4270ecb17911a8b7ad" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/6c05edb11fbeff9e2b324b4270ecb17911a8b7ad", - "reference": "6c05edb11fbeff9e2b324b4270ecb17911a8b7ad", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-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": "2019-01-24T22:05:03+00:00" - }, - { - "name": "symfony/yaml", - "version": "v4.2.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "d461670ee145092b7e2a56c1da7118f19cadadb0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d461670ee145092b7e2a56c1da7118f19cadadb0", - "reference": "d461670ee145092b7e2a56c1da7118f19cadadb0", - "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.2-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": "2019-01-16T20:35:37+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.6.1", - "source": { - "type": "git", - "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2a7dcf7e3e02dc5e701004e51a6f304b713107d5", - "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5", - "shasum": "" - }, - "require": { - "php": ">=5.3.9", - "symfony/polyfill-ctype": "^1.9" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.6-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": "2019-01-29T11:11:52+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0", - "symfony/polyfill-ctype": "^1.8" - }, - "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-12-25T11:19:39+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/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/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/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/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php similarity index 51% rename from dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetBillingAddressOnCartTest.php rename to dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php index 62fae71fa79f5..1a93c011e80a8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetBillingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php @@ -5,17 +5,14 @@ */ declare(strict_types=1); -namespace Magento\GraphQl\Quote; +namespace Magento\GraphQl\Quote\Customer; -use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Integration\Api\CustomerTokenServiceInterface; -use Magento\Multishipping\Helper\Data; -use Magento\Quote\Model\Quote; +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; -use Magento\TestFramework\ObjectManager; /** * Test for set billing address on cart mutation @@ -28,34 +25,36 @@ class SetBillingAddressOnCartTest 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 + * @magentoApiDataFixture Magento/Customer/_files/customer.php */ - public function testSetNewGuestBillingAddressOnCart() + public function testSetNewBillingAddress() { - $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 { @@ -87,12 +86,17 @@ public function testSetNewGuestBillingAddressOnCart() city postcode telephone + country { + code + label + } + address_type } } } } QUERY; - $response = $this->graphQlQuery($query); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); $cartResponse = $response['setBillingAddressOnCart']['cart']; @@ -103,15 +107,11 @@ public function testSetNewGuestBillingAddressOnCart() /** * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php */ - public function testSetNewGuestBillingAddressOnUseForShippingCart() + public function testSetNewBillingAddressWithUseForShippingParameter() { - $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 { @@ -144,6 +144,11 @@ public function testSetNewGuestBillingAddressOnUseForShippingCart() city postcode telephone + country { + code + label + } + address_type } shipping_addresses { firstname @@ -153,12 +158,17 @@ public function testSetNewGuestBillingAddressOnUseForShippingCart() city postcode telephone + country { + code + label + } + address_type } } } } QUERY; - $response = $this->graphQlQuery($query); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); $cartResponse = $response['setBillingAddressOnCart']['cart']; @@ -167,20 +177,17 @@ public function testSetNewGuestBillingAddressOnUseForShippingCart() self::assertArrayHasKey('shipping_addresses', $cartResponse); $shippingAddressResponse = current($cartResponse['shipping_addresses']); $this->assertNewAddressFields($billingAddressResponse); - $this->assertNewAddressFields($shippingAddressResponse); + $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 testSetSavedBillingAddressOnCartByGuest() + public function testSetBillingAddressFromAddressBook() { - $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 { @@ -189,7 +196,7 @@ public function testSetSavedBillingAddressOnCartByGuest() cart_id: "$maskedQuoteId" billing_address: { customer_address_id: 1 - } + } } ) { cart { @@ -201,36 +208,63 @@ public function testSetSavedBillingAddressOnCartByGuest() city postcode telephone + country { + code + label + } } } } } QUERY; - self::expectExceptionMessage('The current customer isn\'t authorized.'); - $this->graphQlQuery($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 testSetNewRegisteredCustomerBillingAddressOnCart() + public function testSetNotExistedBillingAddressFromAddressBook() { - $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); + $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()); + } - $headerMap = $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 { @@ -238,7 +272,8 @@ public function testSetNewRegisteredCustomerBillingAddressOnCart() input: { cart_id: "$maskedQuoteId" billing_address: { - address: { + customer_address_id: 1 + address: { firstname: "test firstname" lastname: "test lastname" company: "test company" @@ -249,55 +284,67 @@ public function testSetNewRegisteredCustomerBillingAddressOnCart() country_code: "US" telephone: "88776655" save_in_address_book: false - } + } } } ) { cart { billing_address { - firstname - lastname - company - street city - postcode - telephone } } } } QUERY; - $response = $this->graphQlQuery($query, [], '', $headerMap); - self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); - $cartResponse = $response['setBillingAddressOnCart']['cart']; - self::assertArrayHasKey('billing_address', $cartResponse); - $billingAddressResponse = $cartResponse['billing_address']; - $this->assertNewAddressFields($billingAddressResponse); + self::expectExceptionMessage( + 'The billing address cannot contain "customer_address_id" and "address" at the same time.' + ); + $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 + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php */ - public function testSetSavedRegisteredCustomerBillingAddressOnCart() + public function testSetBillingAddressToGuestCart() { - $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->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->quote->setCustomerId(1); - $this->quoteResource->save($this->quote); - $headerMap = $this->getHeaderMap(); + $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 { @@ -311,33 +358,58 @@ public function testSetSavedRegisteredCustomerBillingAddressOnCart() ) { cart { billing_address { - firstname - lastname - company - street city - postcode - telephone } } } } QUERY; - $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); - self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); - $cartResponse = $response['setBillingAddressOnCart']['cart']; - self::assertArrayHasKey('billing_address', $cartResponse); - $billingAddressResponse = $cartResponse['billing_address']; - $this->assertSavedBillingAddressFields($billingAddressResponse); + $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 $billingAddressResponse + * @param array $addressResponse + * @param string $addressType */ - private function assertNewAddressFields(array $billingAddressResponse): void + private function assertNewAddressFields(array $addressResponse, string $addressType = 'BILLING'): void { $assertionMap = [ ['response_field' => 'firstname', 'expected_value' => 'test firstname'], @@ -346,10 +418,12 @@ private function assertNewAddressFields(array $billingAddressResponse): void ['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' => $addressType] ]; - $this->assertResponseFields($billingAddressResponse, $assertionMap); + $this->assertResponseFields($addressResponse, $assertionMap); } /** @@ -366,7 +440,8 @@ private function assertSavedBillingAddressFields(array $billingAddressResponse): ['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' => 'telephone', 'expected_value' => '3468676'], + ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], ]; $this->assertResponseFields($billingAddressResponse, $assertionMap); @@ -374,34 +449,41 @@ private function assertSavedBillingAddressFields(array $billingAddressResponse): /** * @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; } - public function tearDown() + /** + * @param string $reversedQuoteId + * @return string + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string { - /** @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 - ); + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ - $config = ObjectManager::getInstance()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $config->reinit(); + 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 f0038351bcdcf..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartTest.php +++ /dev/null @@ -1,169 +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']['shipping_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 - } - shipping_addresses { - firstname, - lastname - } - } -} - -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 d60876e7c0be4..4916bb2aa78c3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php @@ -10,7 +10,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Multishipping\Helper\Data; -use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; @@ -28,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 { @@ -89,6 +90,11 @@ public function testSetNewGuestShippingAddressOnCart() city postcode telephone + country { + code + label + } + address_type } } } @@ -105,15 +111,12 @@ public function testSetNewGuestShippingAddressOnCart() /** * @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 { @@ -129,33 +132,22 @@ public function testSetSavedShippingAddressOnCartByGuest() ) { cart { shipping_addresses { - firstname - lastname - company - street city - postcode - telephone } } } } QUERY; - self::expectExceptionMessage('The current customer isn\'t authorized.'); $this->graphQlQuery($query); } /** * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @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 { @@ -164,10 +156,18 @@ 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 + } } ] } @@ -181,38 +181,33 @@ public function testSetMultipleShippingAddressesOnCartByGuest() city postcode telephone + country { + label + code + } + address_type } } } } QUERY; - /** @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(); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); - self::expectExceptionMessage('You cannot specify multiple shipping addresses.'); - $this->graphQlQuery($query); + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewShippingAddressFields($shippingAddressResponse); } /** * @magentoApiDataFixture Magento/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 { @@ -221,19 +216,7 @@ 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 } ] } @@ -252,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 { @@ -282,45 +298,70 @@ public function testSetShippingAddressOnCartWithNoAddresses() ) { cart { shipping_addresses { - firstname - lastname - company - street city - postcode - telephone } } } } QUERY; - self::expectExceptionMessage( - 'The shipping address must contain either "customer_address_id" or "address".' - ); $this->graphQlQuery($query); } /** * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @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 { @@ -341,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 { shipping_addresses { - firstname - lastname - company - street 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('shipping_addresses', $cartResponse); - $shippingAddressResponse = current($cartResponse['shipping_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 { @@ -405,25 +448,14 @@ public function testSetSavedRegisteredCustomerShippingAddressOnCart() ) { cart { shipping_addresses { - firstname - lastname - company - street - city postcode - telephone } } } } QUERY; - $response = $this->graphQlQuery($query, [], '', $headerMap); - self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); - $cartResponse = $response['setShippingAddressesOnCart']['cart']; - self::assertArrayHasKey('shipping_addresses', $cartResponse); - $shippingAddressResponse = current($cartResponse['shipping_addresses']); - $this->assertSavedShippingAddressFields($shippingAddressResponse); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer2@search.example.com')); } /** @@ -440,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); @@ -468,19 +502,44 @@ 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 */ 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 1c6679ee30f29..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']['shipping_addresses']; - self::assertCount(1, $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, - shipping_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/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/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/MoveCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/MoveCategoryEntityTest.xml index a5758fe1d1346..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 @@ -17,8 +17,7 @@ </variation> <variation name="MoveCategoryEntityTestVariation2" summary="Move default subcategory with anchored parent to default subcategory" ticketId="MAGETWO-21202"> <data name="tag" xsi:type="string">mftf_migrated:yes</data> - <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="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" /> @@ -28,9 +27,8 @@ </variation> <variation name="MoveCategoryEntityTestVariation3" summary="Move default anchored subcategory with anchored parent to default subcategory" ticketId="MAGETWO-21202"> <data name="tag" xsi:type="string">mftf_migrated:yes</data> - <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="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/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/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/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/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/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/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/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/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/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/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/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/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/testsuite/Magento/AuthorizenetAcceptjs/Gateway/ConfigTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/ConfigTest.php index 7e6aeda5a7a6d..a37f927274242 100644 --- a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/ConfigTest.php @@ -8,6 +8,7 @@ namespace Magento\AuthorizenetAcceptjs\Gateway; +use Magento\Framework\Config\Data; use Magento\Payment\Model\Method\Adapter; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; @@ -40,5 +41,11 @@ public function testVerifyConfiguration() $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/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/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/Catalog/_files/product_text_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php similarity index 84% rename from dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php rename to dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php index a9ab0e11312b2..cbc0476efd1b5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php @@ -3,15 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - /* Delete attribute with text_attribute code */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ $attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' ); $attribute->load('text_attribute', 'attribute_code'); $attribute->delete(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php index c57c7c3fd6a92..168073bc6ab74 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php @@ -5,10 +5,10 @@ */ /** Delete all products */ -include dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; +require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; /** Delete text attribute */ -include dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute_rollback.php'; +require dirname(dirname(__DIR__)) . '/Catalog/_files/text_attribute_rollback.php'; -include dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; +require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; -include dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; +require dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php index c57c7c3fd6a92..168073bc6ab74 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php @@ -5,10 +5,10 @@ */ /** Delete all products */ -include dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; +require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; /** Delete text attribute */ -include dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute_rollback.php'; +require dirname(dirname(__DIR__)) . '/Catalog/_files/text_attribute_rollback.php'; -include dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; +require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; -include dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; +require dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; 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/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/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/Elasticsearch/SearchAdapter/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php index 8d80fd8533d6f..dc288a18fadb7 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php @@ -369,18 +369,4 @@ public function dateDataProvider() [['from' => '2000-02-01T00:00:00Z', 'to' => ''], 0], ]; } - - public function filterByAttributeValuesDataProvider() - { - $variations = parent::filterByAttributeValuesDataProvider(); - - $variations['quick search by date'] = [ - 'quick_search_container', - [ - 'search_term' => '2000-10-30', - ], - ]; - - return $variations; - } } diff --git a/dev/tests/integration/testsuite/Magento/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/Search/_files/filterable_attributes.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php index f4f3337a253c0..b09af48b5f943 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php @@ -20,10 +20,6 @@ CategorySetup::class, ['resourceName' => 'catalog_setup'] ); -$productEntityTypeId = $installer->getEntityTypeId( - \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE -); - $selectOptions = []; $selectAttributes = []; foreach (range(1, 2) as $index) { @@ -34,7 +30,7 @@ $selectAttribute->setData( [ 'attribute_code' => 'select_attribute_' . $index, - 'entity_type_id' => $productEntityTypeId, + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), 'is_global' => 1, 'is_user_defined' => 1, 'frontend_input' => 'select', @@ -60,8 +56,7 @@ ); $selectAttribute->save(); /* Assign attribute to attribute set */ - $installer->addAttributeToGroup($productEntityTypeId, 'Default', 'General', $selectAttribute->getId()); - + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $selectAttribute->getId()); /** @var $selectOptions Collection */ $selectOption = Bootstrap::getObjectManager()->create( Collection::class @@ -70,26 +65,6 @@ $selectAttributes[$index] = $selectAttribute; $selectOptions[$index] = $selectOption; } - -$dateAttribute = Bootstrap::getObjectManager()->create(Attribute::class); -$dateAttribute->setData( - [ - 'attribute_code' => 'date_attribute', - 'entity_type_id' => $productEntityTypeId, - 'is_global' => 1, - 'is_filterable' => 1, - 'backend_type' => 'datetime', - 'frontend_input' => 'date', - 'frontend_label' => 'Test Date', - 'is_searchable' => 1, - 'is_filterable_in_search' => 1, - ] -); -$dateAttribute->save(); -/* Assign attribute to attribute set */ -$installer->addAttributeToGroup($productEntityTypeId, 'Default', 'General', $dateAttribute->getId()); - -$productAttributeSetId = $installer->getAttributeSetId($productEntityTypeId, 'Default'); /* Create simple products per each first attribute option */ foreach ($selectOptions[1] as $option) { /** @var $product Product */ @@ -99,7 +74,7 @@ $product->setTypeId( Type::TYPE_SIMPLE )->setAttributeSetId( - $productAttributeSetId + $installer->getAttributeSetId('catalog_product', 'Default') )->setWebsiteIds( [1] )->setName( @@ -117,7 +92,6 @@ )->setStockData( ['use_config_manage_stock' => 1, 'qty' => 5, 'is_in_stock' => 1] )->save(); - Bootstrap::getObjectManager()->get( Action::class )->updateAttributes( @@ -125,7 +99,6 @@ [ $selectAttributes[1]->getAttributeCode() => $option->getId(), $selectAttributes[2]->getAttributeCode() => $selectOptions[2]->getLastItem()->getId(), - $dateAttribute->getAttributeCode() => '10/30/2000', ], $product->getStoreId() ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php index fd413726b2637..18a5372d06d98 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php @@ -13,7 +13,6 @@ $registry = Bootstrap::getObjectManager()->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); - /** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $productCollection = Bootstrap::getObjectManager() ->create(Product::class) @@ -21,26 +20,17 @@ foreach ($productCollection as $product) { $product->delete(); } - /** @var $attribute Attribute */ $attribute = Bootstrap::getObjectManager()->create( Attribute::class ); /** @var $installer CategorySetup */ $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); -$productEntityTypeId = $installer->getEntityTypeId( - \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE -); foreach (range(1, 2) as $index) { - $attribute->loadByCode($productEntityTypeId, 'select_attribute_' . $index); + $attribute->loadByCode($installer->getEntityTypeId('catalog_product'), 'select_attribute_' . $index); if ($attribute->getId()) { $attribute->delete(); } } -$attribute->loadByCode($productEntityTypeId, 'date_attribute'); -if ($attribute->getId()) { - $attribute->delete(); -} - $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/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/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/Search/Model/SynonymReaderTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php index 2d0020ba22680..b9ba89ba53144 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php @@ -48,22 +48,7 @@ public static function loadByPhraseDataProvider() ['synonyms' => 'queen,monarch', 'store_id' => 1, 'website_id' => 0], ['synonyms' => 'british,english', 'store_id' => 1, 'website_id' => 0] ] - ], - [ - 'query_value', [] - ], - [ - 'query_value+', [] - ], - [ - 'query_value-', [] - ], - [ - 'query_@value', [] - ], - [ - 'query_value+@', [] - ], + ] ]; } diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/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/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/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/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/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/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/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 837fef7a1935d..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 @@ -208,3 +208,7 @@ 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/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/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/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 709a432e73b0c..3feb4b4122843 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php @@ -8,43 +8,46 @@ namespace Magento\Framework\Encryption\Test\Unit; +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 \Magento\Framework\Encryption\Encryptor + * @var Encryptor */ private $encryptor; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Random | \PHPUnit_Framework_MockObject_MockObject */ private $randomGeneratorMock; /** - * @var KeyValidator|\PHPUnit_Framework_MockObject_MockObject + * @var KeyValidator | \PHPUnit_Framework_MockObject_MockObject */ private $keyValidatorMock; protected function setUp() { - $this->randomGeneratorMock = $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)); + ->willReturn(self::CRYPT_KEY_1); $this->keyValidatorMock = $this->createMock(KeyValidator::class); $this->encryptor = (new ObjectManager($this))->getObject( - \Magento\Framework\Encryption\Encryptor::class, + Encryptor::class, [ 'random' => $this->randomGeneratorMock, 'deploymentConfig' => $deploymentConfigMock, @@ -53,7 +56,7 @@ protected function setUp() ); } - public function testGetHashNoSalt() + public function testGetHashNoSalt(): void { $this->randomGeneratorMock->expects($this->never())->method('getRandomString'); $expected = '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'; @@ -61,7 +64,7 @@ public function testGetHashNoSalt() $this->assertEquals($expected, $actual); } - public function testGetHashSpecifiedSalt() + public function testGetHashSpecifiedSalt(): void { $this->randomGeneratorMock->expects($this->never())->method('getRandomString'); $expected = '13601bda4ea78e55a07b98866d2be6be0744e3866f13c00c811cab608a28f322:salt:1'; @@ -69,26 +72,26 @@ public function testGetHashSpecifiedSalt() $this->assertEquals($expected, $actual); } - public function testGetHashRandomSaltDefaultLength() + public function testGetHashRandomSaltDefaultLength(): void { $salt = '-----------random_salt----------'; $this->randomGeneratorMock ->expects($this->once()) ->method('getRandomString') ->with(32) - ->will($this->returnValue($salt)); + ->willReturn($salt); $expected = 'a1c7fc88037b70c9be84d3ad12522c7888f647915db78f42eb572008422ba2fa:' . $salt . ':1'; $actual = $this->encryptor->getHash('password', true); $this->assertEquals($expected, $actual); } - public function testGetHashRandomSaltSpecifiedLength() + public function testGetHashRandomSaltSpecifiedLength(): void { $this->randomGeneratorMock ->expects($this->once()) ->method('getRandomString') ->with(11) - ->will($this->returnValue('random_salt')); + ->willReturn('random_salt'); $expected = '4c5cab8dd00137d11258f8f87b93fd17bd94c5026fc52d3c5af911dd177a2611:random_salt:1'; $actual = $this->encryptor->getHash('password', 11); $this->assertEquals($expected, $actual); @@ -101,7 +104,7 @@ public function testGetHashRandomSaltSpecifiedLength() * * @dataProvider validateHashDataProvider */ - public function testValidateHash($password, $hash, $expected) + public function testValidateHash($password, $hash, $expected): void { $actual = $this->encryptor->validateHash($password, $hash); $this->assertEquals($expected, $actual); @@ -110,7 +113,7 @@ public function testValidateHash($password, $hash, $expected) /** * @return array */ - public function validateHashDataProvider() + public function validateHashDataProvider(): array { return [ ['password', 'hash:salt:1', false], @@ -125,13 +128,13 @@ 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)); + ->willReturn($key); $model = new Encryptor($this->randomGeneratorMock, $deploymentConfigMock); $value = 'arbitrary_string'; $this->assertEquals($value, $model->encrypt($value)); @@ -140,7 +143,7 @@ public function testEncryptWithEmptyKey($key) /** * @return array */ - public function encryptWithEmptyKeyDataProvider() + public function encryptWithEmptyKeyDataProvider(): array { return [[null], [0], [''], ['0']]; } @@ -150,13 +153,13 @@ 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)); + ->willReturn($key); $model = new Encryptor($this->randomGeneratorMock, $deploymentConfigMock); $value = 'arbitrary_string'; $this->assertEquals('', $model->decrypt($value)); @@ -165,12 +168,12 @@ 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.'; @@ -178,15 +181,14 @@ public function testEncrypt() $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->encryptor->encrypt($message); @@ -194,7 +196,7 @@ public function testDecrypt() $this->assertEquals($message, $this->encryptor->decrypt($encrypted)); } - public function testLegacyDecrypt() + public function testLegacyDecrypt(): void { // sample data to encrypt $data = '0:2:z3a4ACpkU35W6pV692U4ueCVQP0m0v0p:' . @@ -203,8 +205,7 @@ public function testLegacyDecrypt() $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); @@ -212,17 +213,17 @@ 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)); + ->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->randomGeneratorMock, $deploymentConfigMock); @@ -236,7 +237,7 @@ public function testEncryptDecryptNewKeyAdded() $this->assertSame($data, $decryptedData, 'Encryptor failed to decrypt data encrypted by old keys.'); } - public function testValidateKey() + public function testValidateKey(): void { $this->keyValidatorMock->method('isValid')->willReturn(true); $this->encryptor->validateKey(self::CRYPT_KEY_1); @@ -245,7 +246,7 @@ public function testValidateKey() /** * @expectedException \Exception */ - public function testValidateKeyInvalid() + public function testValidateKeyInvalid(): void { $this->keyValidatorMock->method('isValid')->willReturn(false); $this->encryptor->validateKey('----- '); @@ -254,7 +255,7 @@ public function testValidateKeyInvalid() /** * @return array */ - public function useSpecifiedHashingAlgoDataProvider() + public function useSpecifiedHashingAlgoDataProvider(): array { return [ ['password', 'salt', Encryptor::HASH_VERSION_MD5, @@ -276,7 +277,7 @@ public function useSpecifiedHashingAlgoDataProvider() * @param $hashAlgo * @param $expected */ - public function testGetHashMustUseSpecifiedHashingAlgo($password, $salt, $hashAlgo, $expected) + public function testGetHashMustUseSpecifiedHashingAlgo($password, $salt, $hashAlgo, $expected): void { $hash = $this->encryptor->getHash($password, $salt, $hashAlgo); $this->assertEquals($expected, $hash); 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/HTTP/Adapter/Curl.php b/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php index 0e51c64661a4b..bc833bf3bb2d4 100644 --- a/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php +++ b/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php @@ -170,7 +170,6 @@ public function write($method, $url, $http_ver = '1.1', $headers = [], $body = ' // set url to post to curl_setopt($this->_getResource(), CURLOPT_URL, $url); - curl_setopt($this->_getResource(), CURLOPT_HTTP_VERSION, $http_ver); curl_setopt($this->_getResource(), CURLOPT_RETURNTRANSFER, true); if ($method == \Zend_Http_Client::POST) { curl_setopt($this->_getResource(), CURLOPT_POST, true); 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/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/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/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/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 11501d4a7fee9..4499c314ce6ca 100644 --- a/lib/web/css/source/lib/_resets.less +++ b/lib/web/css/source/lib/_resets.less @@ -4,7 +4,7 @@ // */ // -// Resetes +// Resets // _____________________________________________ // diff --git a/lib/web/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/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/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/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/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); }